The Fix
Fixes an issue with cronjobs that use day of the month and negative UTC timezones, preventing tasks from being skipped due to incorrect scheduling logic.
Based on closed celery/celery issue #8052 · PR/commit linked
@@ -72,8 +72,8 @@ def remaining_estimate(self, last_run_at):
raise NotImplementedError()
- def maybe_make_aware(self, dt):
- return maybe_make_aware(dt, self.tz)
+ def maybe_make_aware(self, dt, naive_as_utc=True):
import pytz
import datetime
from celery import schedules
tz = pytz.timezone('America/Los_Angeles')
def normalized_time(year, month, day, hour, min, sec):
return tz.normalize(
pytz.utc.localize(datetime.datetime(year, month, day, hour, min, sec))
)
def run(last_run_at, now):
class Foo(schedules.crontab):
def __init__(self, *args, **kwargs):
self.tz = tz
super().__init__(*args, **kwargs)
def now(self):
return now
foo = Foo(minute='0', hour='*', day_of_week='*', day_of_month='28-31', month_of_year='*')
return foo.is_due(last_run_at=last_run_at)
for hour in range(0, 23):
last_run_at = normalized_time(2023, 1, 29, hour, 0, 0)
now = normalized_time(2023, 1, 29, hour, 0, 0) + datetime.timedelta(hours=1)
result = run(last_run_at, now)
if not result.is_due or result.next != 3600:
print(f"Fail: {last_run_at}: {result}")
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nFixes an issue with cronjobs that use day of the month and negative UTC timezones, preventing tasks from being skipped due to incorrect scheduling logic.\nWhen NOT to use: Do not use this fix if your application relies on the previous scheduling behavior.\n\n
Why This Fix Works in Production
- Trigger: print(f"Fail: {last_run_at}: {result}")
- Mechanism: The scheduling logic incorrectly skips valid candidates due to timezone handling
- If left unfixed, this can cause silent data inconsistencies that propagate (bad cache entries, incorrect downstream decisions).
Why This Breaks in Prod
- The scheduling logic incorrectly skips valid candidates due to timezone handling
- Production symptom (often without a traceback): print(f"Fail: {last_run_at}: {result}")
Proof / Evidence
- GitHub issue: #8052
- Fix PR: https://github.com/celery/celery/pull/8053
- Reproduced locally: No (not executed)
- Last verified: 2026-02-12
- Confidence: 0.70
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.49
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“a proper failing test would be great starting point I guess”
“Hey @pkyosx :wave:, Thank you for opening an issue”
“if you can dig it further that would be highly appreciable.”
“I believe the problem lies within the following code Because when last_run_at='2023-01-28 23:00:00-08:00', it will try to find the schedule to the next day”
Failure Signature (Search String)
- print(f"Fail: {last_run_at}: {result}")
- Fail: 2023-01-28 22:00:00-08:00: schedstate(is_due=True, next=2595600.0)
Copy-friendly signature
Failure Signature
-----------------
print(f"Fail: {last_run_at}: {result}")
Fail: 2023-01-28 22:00:00-08:00: schedstate(is_due=True, next=2595600.0)
Error Message
Signature-only (no traceback captured)
Error Message
-------------
print(f"Fail: {last_run_at}: {result}")
Fail: 2023-01-28 22:00:00-08:00: schedstate(is_due=True, next=2595600.0)
Minimal Reproduction
import pytz
import datetime
from celery import schedules
tz = pytz.timezone('America/Los_Angeles')
def normalized_time(year, month, day, hour, min, sec):
return tz.normalize(
pytz.utc.localize(datetime.datetime(year, month, day, hour, min, sec))
)
def run(last_run_at, now):
class Foo(schedules.crontab):
def __init__(self, *args, **kwargs):
self.tz = tz
super().__init__(*args, **kwargs)
def now(self):
return now
foo = Foo(minute='0', hour='*', day_of_week='*', day_of_month='28-31', month_of_year='*')
return foo.is_due(last_run_at=last_run_at)
for hour in range(0, 23):
last_run_at = normalized_time(2023, 1, 29, hour, 0, 0)
now = normalized_time(2023, 1, 29, hour, 0, 0) + datetime.timedelta(hours=1)
result = run(last_run_at, now)
if not result.is_due or result.next != 3600:
print(f"Fail: {last_run_at}: {result}")
What Broke
Tasks are skipped unexpectedly due to incorrect scheduling logic in specific timezones.
Why It Broke
The scheduling logic incorrectly skips valid candidates due to timezone handling
Fix Options (Details)
Option A — Apply the official fix
Fixes an issue with cronjobs that use day of the month and negative UTC timezones, preventing tasks from being skipped due to incorrect scheduling logic.
Fix reference: https://github.com/celery/celery/pull/8053
Last verified: 2026-02-12. Validate in your environment.
When NOT to Use This Fix
- Do not use this fix if your application relies on the previous scheduling behavior.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Capture the exact failing error string in logs and tests so you can reproduce via a minimal script.
- Pin production dependencies and upgrade only with a reproducible test that hits the failing path.
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.