<h1 dir="auto">Checklist</h1>

<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have verified that the issue exists against the <code class="notranslate">main</code> branch of Celery.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have read the relevant section in the<br>
<a href="https://docs.celeryq.dev/en/main/contributing.html#other-bugs" rel="nofollow">contribution guide</a><br>
on reporting bugs.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have checked the <a href="https://github.com/celery/celery/issues?q=is%3Aissue+label%3A%22Issue+Type%3A+Bug+Report%22+-label%3A%22Category%3A+Documentation%22">issues list</a><br>
for similar or identical bug reports.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have checked the <a href="https://github.com/celery/celery/pulls?q=is%3Apr+label%3A%22PR+Type%3A+Bugfix%22+-label%3A%22Category%3A+Documentation%22">pull requests list</a><br>
for existing proposed fixes.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have checked the <a href="https://github.com/celery/celery/commits/main">commit log</a><br>
to find out if the bug was already fixed in the main branch.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have included all related issues and possible duplicate issues<br>
in this issue (If there are none, check this box anyway).</li>
</ul>
<h2 dir="auto">Mandatory Debugging Information</h2>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have included the output of <code class="notranslate">celery -A proj report</code> in the issue.<br>
(if you are not able to do this, then at least specify the Celery<br>
version affected).</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have verified that the issue exists against the <code class="notranslate">main</code> branch of Celery.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have included the contents of <code class="notranslate">pip freeze</code> in the issue.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have included all the versions of all the external dependencies required<br>
to reproduce this bug.</li>
</ul>
<h2 dir="auto">Environment &amp; Settings</h2>

<p dir="auto"><strong>Celery version</strong>: 5.2.7</p>

<details>
<summary><b><code class="notranslate">celery report</code> Output:</b></summary>
<code class="notranslate">
import os
from celery import Celery
<p dir="auto">os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')</p>
</code><p dir="auto"><code class="notranslate">app = Celery('proj')<br>
app.conf.update({<br>
'broker_url': 'redis://localhost:8000/11',<br>
'result_backend': 'django-db',<br>
'accept_content': ['json'],<br>
'task_serializer': 'json',<br>
'result_serializer': 'json',<br>
'worker_force_execv': True,<br>
'worker_concurrency': 10,<br>
'task_time_limit': 60 * 60,<br>
'worker_max_tasks_per_child': 200,<br>
'beat_tz_aware': False,<br>
'result_extended': True,<br>
})<br>
app.conf.timezone = 'Asia/Shanghai'<br>
app.conf.enable_utc = False<br>
app.autodiscover_tasks()<br>
</code></p>
</details>
<h1 dir="auto">Steps to Reproduce</h1>
<h2 dir="auto">Required Dependencies</h2>

<ul dir="auto">
<li><strong>Minimal Python Version</strong>: 3.8.6</li>
<li><strong>Minimal Celery Version</strong>: 5.2.7</li>
<li><strong>Minimal OS and/or Kernel Version</strong>: CentOS Linux release 7.9.2009 (Core)</li>
</ul>
<h3 dir="auto">Python Packages</h3>

<details>
<summary><b><code class="notranslate">pip freeze</code> Output:</b></summary>
<code class="notranslate">
aioredis==1.3.1
amqp==5.1.1
ansible-runner==2.2.0
asgiref==3.5.2
async-timeout==4.0.2
attrs==21.4.0
autobahn==22.5.1
Automat==20.2.0
backports.zoneinfo==0.2.1
billiard==3.6.4.0
celery==5.2.7
certifi==2022.6.15
cffi==1.15.0
channels==3.0.4
channels-redis==3.4.0
charset-normalizer==2.1.0
click==8.1.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
constantly==15.1.0
cron-descriptor==1.3.0
cryptography==37.0.2
cx-Oracle==8.3.0
daphne==3.0.2
Deprecated==1.2.13
Django==4.0.5
django-celery-beat==2.5.0
django-celery-results==2.5.1
django-cors-headers==3.13.0
django-redis==5.2.0
django-simpleui==2023.3.1
django-timezone-field==5.0
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2
docopt==0.6.2
docutils==0.18.1
flower==1.2.0
hiredis==2.0.0
humanize==4.6.0
hyperlink==21.0.0
idna==3.3
incremental==21.3.0
kombu==5.2.4
ldap3==2.9.1
lockfile==0.12.2
msgpack==1.0.4
mysqlclient==2.1.0
packaging==21.3
pexpect==4.8.0
pipreqs==0.4.11
prometheus-client==0.16.0
prompt-toolkit==3.0.29
ptyprocess==0.7.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21
PyJWT==2.6.0
pyOpenSSL==22.0.0
pyparsing==3.0.9
python-crontab==2.6.0
python-daemon==2.3.0
python-dateutil==2.8.2
pytz==2022.1
PyYAML==6.0
redis==4.3.3
requests==2.28.1
service-identity==21.1.0
six==1.16.0
sqlparse==0.4.2
supervisor==4.2.4
tornado==6.3.1
Twisted==22.4.0
txaio==22.2.1
typing_extensions==4.2.0
tzdata==2022.2
urllib3==1.26.9
vine==5.0.0
wcwidth==0.2.5
wrapt==1.14.1
yapf==0.32.0
yarg==0.1.9
zope.interface==5.4.0
</code>
</details>
Steps to reproduce
<ol dir="auto">
<li>Start Celery with the following configuration:</li>
</ol>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="#proj/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings')

app = Celery('proj')
app.conf.update({
    'broker_url': 'redis://localhost:8000/11',
    'result_backend': 'django-db',
    'accept_content': ['json'],
    'task_serializer': 'json',
    'result_serializer': 'json',
    'worker_force_execv': True,
    'worker_concurrency': 10,
    'task_time_limit': 60 * 60,
    'worker_max_tasks_per_child': 200,
    'beat_tz_aware': False,
    'result_extended': True,
})
app.conf.timezone = 'Asia/Shanghai'
app.conf.enable_utc = False
app.autodiscover_tasks()"><pre class="notranslate"><span class="pl-c">#proj/celery.py</span>
<span class="pl-k">import</span> <span class="pl-s1">os</span>
<span class="pl-k">from</span> <span class="pl-s1">celery</span> <span class="pl-k">import</span> <span class="pl-v">Celery</span>

<span class="pl-s1">os</span>.<span class="pl-s1">environ</span>.<span class="pl-en">setdefault</span>(<span class="pl-s">'DJANGO_SETTINGS_MODULE'</span>, <span class="pl-s">'proj.settings'</span>)

<span class="pl-s1">app</span> <span class="pl-c1">=</span> <span class="pl-v">Celery</span>(<span class="pl-s">'proj'</span>)
<span class="pl-s1">app</span>.<span class="pl-s1">conf</span>.<span class="pl-en">update</span>({
    <span class="pl-s">'broker_url'</span>: <span class="pl-s">'redis://localhost:8000/11'</span>,
    <span class="pl-s">'result_backend'</span>: <span class="pl-s">'django-db'</span>,
    <span class="pl-s">'accept_content'</span>: [<span class="pl-s">'json'</span>],
    <span class="pl-s">'task_serializer'</span>: <span class="pl-s">'json'</span>,
    <span class="pl-s">'result_serializer'</span>: <span class="pl-s">'json'</span>,
    <span class="pl-s">'worker_force_execv'</span>: <span class="pl-c1">True</span>,
    <span class="pl-s">'worker_concurrency'</span>: <span class="pl-c1">10</span>,
    <span class="pl-s">'task_time_limit'</span>: <span class="pl-c1">60</span> <span class="pl-c1">*</span> <span class="pl-c1">60</span>,
    <span class="pl-s">'worker_max_tasks_per_child'</span>: <span class="pl-c1">200</span>,
    <span class="pl-s">'beat_tz_aware'</span>: <span class="pl-c1">False</span>,
    <span class="pl-s">'result_extended'</span>: <span class="pl-c1">True</span>,
})
<span class="pl-s1">app</span>.<span class="pl-s1">conf</span>.<span class="pl-s1">timezone</span> <span class="pl-c1">=</span> <span class="pl-s">'Asia/Shanghai'</span>
<span class="pl-s1">app</span>.<span class="pl-s1">conf</span>.<span class="pl-s1">enable_utc</span> <span class="pl-c1">=</span> <span class="pl-c1">False</span>
<span class="pl-s1">app</span>.<span class="pl-en">autodiscover_tasks</span>()</pre></div>
<ol start="2" dir="auto">
<li>Add a task to the app:</li>
</ol>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="# myapp/tasks.py
from proj.celery import app
@app.task
def add():
    print('aa')"><pre class="notranslate"><span class="pl-c"># myapp/tasks.py</span>
<span class="pl-k">from</span> <span class="pl-s1">proj</span>.<span class="pl-s1">celery</span> <span class="pl-k">import</span> <span class="pl-s1">app</span>
<span class="pl-en">@<span class="pl-s1">app</span>.<span class="pl-s1">task</span></span>
<span class="pl-k">def</span> <span class="pl-en">add</span>():
    <span class="pl-en">print</span>(<span class="pl-s">'aa'</span>)</pre></div>
<ol start="3" dir="auto">
<li>Attempt to call the task with the following code:</li>
</ol>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="# myapp/views.py
def task_page(request):
    from dbaOps.celery import app
    print(app.tasks.keys())
    return render(request, 'myapp/tasks.html')"><pre class="notranslate"><span class="pl-c"># myapp/views.py</span>
<span class="pl-k">def</span> <span class="pl-en">task_page</span>(<span class="pl-s1">request</span>):
    <span class="pl-k">from</span> <span class="pl-s1">dbaOps</span>.<span class="pl-s1">celery</span> <span class="pl-k">import</span> <span class="pl-s1">app</span>
    <span class="pl-en">print</span>(<span class="pl-s1">app</span>.<span class="pl-s1">tasks</span>.<span class="pl-en">keys</span>())
    <span class="pl-k">return</span> <span class="pl-en">render</span>(<span class="pl-s1">request</span>, <span class="pl-s">'myapp/tasks.html'</span>)</pre></div>
<p dir="auto">Expected behavior<br>
The task myapp.tasks.add should be listed in the output of app.tasks.keys().</p>
<p dir="auto">Actual Behavior<br>
The task myapp.tasks.add does not appear in the output of app.tasks.keys(). However, when I modify the import statement in views.py to from myapp.tasks import app, the task does appear in the list.</p>
<div class="highlight highlight-source-python notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="# myapp/views.py
def task_page(request):
    from myapp.tasks import app
    print(app.tasks.keys())
    return render(request, 'myapp/tasks.html')"><pre class="notranslate"><span class="pl-c"># myapp/views.py</span>
<span class="pl-k">def</span> <span class="pl-en">task_page</span>(<span class="pl-s1">request</span>):
    <span class="pl-k">from</span> <span class="pl-s1">myapp</span>.<span class="pl-s1">tasks</span> <span class="pl-k">import</span> <span class="pl-s1">app</span>
    <span class="pl-en">print</span>(<span class="pl-s1">app</span>.<span class="pl-s1">tasks</span>.<span class="pl-en">keys</span>())
    <span class="pl-k">return</span> <span class="pl-en">render</span>(<span class="pl-s1">request</span>, <span class="pl-s">'myapp/tasks.html'</span>)</pre></div>
<p dir="auto">I would appreciate any help in understanding why this is happening and how to resolve it. Thank you!</p>
<p dir="auto">Related Issues and Possible Duplicates<br>
Related Issues<br>
None<br>
Possible Duplicates<br>
None</p>
<p dir="auto">There's an issue in the implementation of <code class="notranslate">celery.schedules.crontab</code> method that can cause <code class="notranslate">crontab.is_due</code> to claim a schedule is due at a time that is completely unrelated to the given crontab parameters. This appears to happen in cases where the <code class="notranslate">last_run_at</code> value is older than the most recent feasible time the schedule could have run. It looks like that this issue was introduced as part of complex logic added nearly 8 years ago to improve the accuracy of time remaining estimates for <code class="notranslate">crontab</code> periodic tasks: <a class="commit-link" data-hovercard-type="commit" data-hovercard-url="https://github.com/celery/celery/commit/4ed89ec49582b540149cf06047f091ebd20fb300/hovercard" href="https://github.com/celery/celery/commit/4ed89ec49582b540149cf06047f091ebd20fb300"><tt>4ed89ec</tt></a></p>
<h2 dir="auto">Checklist</h2>
<p dir="auto">Issue first observed in a celery deployment running celery v3.1.19</p>
<p dir="auto">From inspecting the <code class="notranslate">celery.schedules.crontab</code> code it appears likely that this issue is present in all celery versions as far back as v2.1.0 through to v4.1.1 .</p>
<p dir="auto">Unit tests (please see below) confirm issue is still present in master (<a class="commit-link" data-hovercard-type="commit" data-hovercard-url="https://github.com/celery/celery/commit/b599b96960be9dd42b3dee82a58bd1d711df0317/hovercard" href="https://github.com/celery/celery/commit/b599b96960be9dd42b3dee82a58bd1d711df0317"><tt>b599b96</tt></a> at time of writing).</p>
<h2 dir="auto">Steps to reproduce</h2>
<p dir="auto">Please apply this patch to celery master branch, remove <code class="notranslate">@skip.todo</code> from the first new test, run unit tests, observe the first of these added unit tests fails:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="diff --git a/t/unit/app/test_schedules.py b/t/unit/app/test_schedules.py
index a7b3025..0340461 100644
--- a/t/unit/app/test_schedules.py
+++ b/t/unit/app/test_schedules.py
@@ -26,6 +26,18 @@ def patch_crontab_nowfun(cls, retval):
         cls.nowfun = prev_nowfun
 
 
+def is_time_feasible_wrt_crontab_schedule(t, z):
+    # z : celery.schedules.crontab instance
+    t = z.maybe_make_aware(t)
+    return (
+        t.month in z.month_of_year and
+        (t.isoweekday() % 7) in z.day_of_week and
+        t.day in z.day_of_month and
+        t.hour in z.hour and
+        t.minute in z.minute
+    )
+
+
 @skip.unless_module('ephem')
 class test_solar:
 
@@ -803,3 +815,59 @@ class test_crontab_is_due:
             due, remaining = self.yearly.is_due(datetime(2009, 3, 12, 7, 30))
             assert not due
             assert remaining == 4 * 24 * 60 * 60 - 3 * 60 * 60
+
+    @skip.todo('FIXME crontab logic is defective when last_run_at is older than the most recent feasible time wrt schedule')
+    def test_daily_execution_if_last_run_at_was_days_ago_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 1, 7, 30)
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+
+    def test_daily_execution_if_last_run_at_was_the_most_recent_feasible_time_wrt_schedule_in_past_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 9, 7, 30)
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+
+    def test_daily_execution_if_last_run_at_was_more_recent_than_the_most_recent_feasible_time_wrt_schedule_in_past_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 9, 10, 30) # not feasible wrt to current schedule. case can happen if schedule is modified after a run
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert not is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+"><pre class="notranslate"><code class="notranslate">diff --git a/t/unit/app/test_schedules.py b/t/unit/app/test_schedules.py
index a7b3025..0340461 100644
--- a/t/unit/app/test_schedules.py
+++ b/t/unit/app/test_schedules.py
@@ -26,6 +26,18 @@ def patch_crontab_nowfun(cls, retval):
         cls.nowfun = prev_nowfun
 
 
+def is_time_feasible_wrt_crontab_schedule(t, z):
+    # z : celery.schedules.crontab instance
+    t = z.maybe_make_aware(t)
+    return (
+        t.month in z.month_of_year and
+        (t.isoweekday() % 7) in z.day_of_week and
+        t.day in z.day_of_month and
+        t.hour in z.hour and
+        t.minute in z.minute
+    )
+
+
 @skip.unless_module('ephem')
 class test_solar:
 
@@ -803,3 +815,59 @@ class test_crontab_is_due:
             due, remaining = self.yearly.is_due(datetime(2009, 3, 12, 7, 30))
             assert not due
             assert remaining == 4 * 24 * 60 * 60 - 3 * 60 * 60
+
+    @skip.todo('FIXME crontab logic is defective when last_run_at is older than the most recent feasible time wrt schedule')
+    def test_daily_execution_if_last_run_at_was_days_ago_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 1, 7, 30)
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+
+    def test_daily_execution_if_last_run_at_was_the_most_recent_feasible_time_wrt_schedule_in_past_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 9, 7, 30)
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+
+    def test_daily_execution_if_last_run_at_was_more_recent_than_the_most_recent_feasible_time_wrt_schedule_in_past_and_current_time_does_not_match_crontab_schedule_then_execution_is_not_due(self):
+        z = self.crontab(hour=7, minute=30)
+        last_run_at = datetime(2018, 6, 9, 10, 30) # not feasible wrt to current schedule. case can happen if schedule is modified after a run
+        now = datetime(2018, 6, 9, 23, 48)
+        expected_next_execution_time = datetime(2018, 6, 10, 7, 30)
+        expected_remaining = (expected_next_execution_time - now).total_seconds()
+        # check our assumptions
+        assert not is_time_feasible_wrt_crontab_schedule(last_run_at, z)
+        assert not is_time_feasible_wrt_crontab_schedule(now, z)
+        assert is_time_feasible_wrt_crontab_schedule(expected_next_execution_time, z)
+        assert now &lt; expected_next_execution_time
+        assert expected_remaining == (7 * 60 + 30 + 12) * 60
+        # test is_due
+        with patch_crontab_nowfun(z, now):
+            due, remaining = z.is_due(last_run_at=last_run_at)
+            assert remaining == expected_remaining
+            assert not due
+
</code></pre></div>
<h2 dir="auto">Expected behavior</h2>
<p dir="auto">No matter what the value of <code class="notranslate">last_run_at</code> is, <code class="notranslate">crontab.is_due(last_run_at)</code> should never return <code class="notranslate">schedstate(True, rem)</code> when the current time <code class="notranslate">now</code> is not feasible with respect to the given crontab parameters.</p>
<h2 dir="auto">Actual behavior</h2>
<p dir="auto">if <code class="notranslate">last_run_at</code> value is older than the most recent time that is feasible with respect to the given crontab parameters then <code class="notranslate">crontab.is_due(last_run_at)</code> returns <code class="notranslate">schedstate(True, rem)</code>  when the current time is not feasible with respect to the crontab parameters.</p>
<h2 dir="auto">Comments</h2>
<p dir="auto">This behaviour is surprising as it is an undocumented departure from cron-like behaviour. This behaviour is somewhat like an undocumented variant of uncontrollable <code class="notranslate">anacron</code> behaviour. "Uncontrollable" in the sense that unlike <code class="notranslate">anacrontab</code>'s <code class="notranslate">START_HOURS_RANGE</code> parameter - there is no control at all over when tasks will be run when a scheduled execution is missed.</p>
<p dir="auto">We experience this issue in production using the venerable Celery version of 3.1.19 : we have a celery beat process that is backed by a database using custom scheduler code that is derived from the django-celery-beat scheduler. Suppose we stop this celery beat process for some reason such as scheduled maintenance or during a deployment that needs to redeploy code to our celery cluster. When we later restart celery beat again then some or all <code class="notranslate">celery.schedules.crontab</code> scheduled tasks may immediately execute if there was a time during the celery beat downtime period that matches the crontab parameters.</p>
<p dir="auto">One hack that can mitigate this behaviour is updating the "last_run_at" value for all celery crontab scheduled periodic tasks to the current time when celery beat starts, before celery beat makes any calls to the <code class="notranslate">celery.schedules.crontab.is_due</code> method.</p>