Jump to solution
Verify

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

Jump to Verify Open PR/Commit
@@ -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):
repro.py
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}")
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
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
Production impact:
  • 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

Discussion

High-signal excerpts from the issue thread (symptoms, repros, edge-cases).

“a proper failing test would be great starting point I guess”
@auvipy · 2023-02-04 · source
“Hey @pkyosx :wave:, Thank you for opening an issue”
@open-collective-bot · 2023-02-04 · source
“if you can dig it further that would be highly appreciable.”
@auvipy · 2023-02-04 · source
“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”
@pkyosx · 2023-02-04 · source

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
signature.txt
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.txt
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

repro.py
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.

When NOT to use: Do not use this fix if your application relies on the previous scheduling behavior.

Fix reference: https://github.com/celery/celery/pull/8053

Last verified: 2026-02-12. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

When NOT to Use This Fix

  • Do not use this fix if your application relies on the previous scheduling behavior.

Verify Fix

verify
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.