diff --git a/app/models.py b/app/models.py
index 69217b7892845855765b09e0302583776f123915..4832add6af07bc80d2a48b4bb82479538bdbf421 100644
--- a/app/models.py
+++ b/app/models.py
@@ -9,6 +9,7 @@ This module implements the models used in the app.
 :license: BSD 2-Clause, see LICENSE for more details.
 
 """
+import datetime
 import ipaddress
 import string
 import qrcode
@@ -283,10 +284,24 @@ class User(db.Model, UserMixin):
         return Task.query.filter_by(name=name, status=JobStatus.STARTED).first()
 
     def is_task_waiting(self, name):
-        """Return True if a <name> task is waiting (queued or deferred)"""
+        """Return True if a <name> task is waiting
+
+        Waiting means:
+            - queued
+            - deferred if not older than 30 minutes
+
+        A deferred task can stay deferred forever if the task it depends on fails.
+        """
+        thirty_minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=30)
         count = (
             Task.query.filter_by(name=name)
-            .filter(Task.status.in_([JobStatus.DEFERRED, JobStatus.QUEUED]))
+            .filter(
+                (Task.status == JobStatus.QUEUED)
+                | (
+                    (Task.status == JobStatus.DEFERRED)
+                    & (Task.created_at > thirty_minutes_ago)
+                )
+            )
             .count()
         )
         return count > 0
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 0774d78e9cc4c657d47ca635572198ae22b2bc6b..7bace7fd3ce3cac2079cf717bd08e4b8a5078eb9 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -34,6 +34,7 @@ register(factories.MacFactory)
 register(factories.DomainFactory)
 register(factories.CnameFactory)
 register(factories.TagFactory)
+register(factories.TaskFactory)
 
 
 @pytest.fixture(scope="session")
diff --git a/tests/functional/factories.py b/tests/functional/factories.py
index ace920c5d6b41f3764fbc44a230554ff2b872a42..04805e5c67568f18a64a53213229a2eaaa4cb020 100644
--- a/tests/functional/factories.py
+++ b/tests/functional/factories.py
@@ -11,14 +11,10 @@ This module defines models factories.
 """
 import ipaddress
 import factory
-from faker import Factory as FakerFactory
 from app import models
 from . import common
 
 
-faker = FakerFactory.create()
-
-
 class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
     class Meta:
         model = models.User
@@ -210,3 +206,14 @@ class TagFactory(factory.alchemy.SQLAlchemyModelFactory):
         sqlalchemy_session_persistence = "commit"
 
     name = factory.Sequence(lambda n: f"Tag{n}")
+
+
+class TaskFactory(factory.alchemy.SQLAlchemyModelFactory):
+    class Meta:
+        model = models.Task
+        sqlalchemy_session = common.Session
+        sqlalchemy_session_persistence = "commit"
+
+    id = factory.Faker("uuid4")
+    name = factory.Sequence(lambda n: f"task{n}")
+    user = factory.SubFactory(UserFactory)
diff --git a/tests/functional/test_models.py b/tests/functional/test_models.py
index 96aa143c2759dac25255fe949dbcddbedda1f804..8bbdfaedd2112d3e01d20b5bc956f4a8ebf099f6 100644
--- a/tests/functional/test_models.py
+++ b/tests/functional/test_models.py
@@ -9,6 +9,7 @@ This module defines models tests.
 :license: BSD 2-Clause, see LICENSE for more details.
 
 """
+import datetime
 import ipaddress
 import pytest
 from wtforms import ValidationError
@@ -287,3 +288,67 @@ def test_ansible_dynamic_network_scope_group(
     assert group_s1.hosts == [host1_s1, host2_s1]
     assert group_s2.hosts == [host1_s2]
     assert group_s3.hosts == []
+
+
+@pytest.mark.parametrize("status", [None, "FINISHED", "FAILED", "STARTED"])
+def test_no_task_waiting(status, user, task_factory):
+    if status is None:
+        task_factory(name="my-task", status=None)
+    else:
+        task_factory(name="my-task", status=models.JobStatus[status])
+    assert not user.is_task_waiting("my-task")
+
+
+@pytest.mark.parametrize("status", ["QUEUED", "DEFERRED"])
+def test_task_waiting(status, user, task_factory):
+    task_factory(name="my-task", status=models.JobStatus[status])
+    assert user.is_task_waiting("my-task")
+
+
+@pytest.mark.parametrize("minutes", [5, 10, 29])
+def test_task_waiting_with_recent_deferred(minutes, user, task_factory):
+    minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes)
+    task_factory(
+        created_at=minutes_ago, name="my-task", status=models.JobStatus.DEFERRED
+    )
+    assert user.is_task_waiting("my-task")
+
+
+@pytest.mark.parametrize("minutes", [31, 60, 7200])
+def test_no_task_waiting_with_old_deferred(minutes, user, task_factory):
+    minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes)
+    task_factory(
+        created_at=minutes_ago, name="my-task", status=models.JobStatus.DEFERRED
+    )
+    assert not user.is_task_waiting("my-task")
+
+
+@pytest.mark.parametrize("minutes", [5, 30, 7200])
+def test_task_waiting_with_old_queued(minutes, user, task_factory):
+    minutes_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=minutes)
+    task_factory(created_at=minutes_ago, name="my-task", status=models.JobStatus.QUEUED)
+    assert user.is_task_waiting("my-task")
+
+
+def test_get_task_started_when_only_one(user, task_factory):
+    task_factory(name="my-task", status=models.JobStatus.QUEUED)
+    task = task_factory(name="my-task", status=models.JobStatus.STARTED)
+    task_factory(name="my-task", status=models.JobStatus.FINISHED)
+    task_factory(name="my-task", status=models.JobStatus.FAILED)
+    assert user.get_task_started("my-task") == task
+
+
+def test_get_task_started_when_several(user, task_factory):
+    # Only the first started task is returned
+    task1 = task_factory(name="my-task", status=models.JobStatus.STARTED)
+    task_factory(name="my-task", status=models.JobStatus.STARTED)
+    assert user.get_task_started("my-task") == task1
+
+
+def test_get_tasks_in_progress(user, task_factory):
+    task1 = task_factory(name="my-task", status=models.JobStatus.QUEUED)
+    task2 = task_factory(name="my-task", status=models.JobStatus.STARTED)
+    task_factory(name="my-task", status=models.JobStatus.FINISHED)
+    task_factory(name="my-task", status=models.JobStatus.FAILED)
+    task3 = task_factory(name="my-task", status=models.JobStatus.DEFERRED)
+    assert user.get_tasks_in_progress("my-task") == [task1, task2, task3]