Description
The due_at property in RedBeatSchedulerEntry calculates an incorrect next run time. The issue is that remaining_estimate() returns the time delta from now to the next scheduled time, but due_at adds this delta to last_run_at instead of now.
Current Behavior
In redbeat/schedulers.py, the due_at property is implemented as:
@property
def due_at(self):
if self.last_run_at is None:
return self._default_now()
delta = self.schedule.remaining_estimate(self.last_run_at)
if delta is None:
return None
if delta.total_seconds() < 0:
return self._default_now()
return self.last_run_at + delta # <-- This is incorrect
The problem is that remaining_estimate() returns the time until the next scheduled occurrence from now, regardless of what argument is passed. So adding this delta to last_run_at produces an incorrect result.
Reproduction
from celery.schedules import crontab
from datetime import datetime, timezone, timedelta
# Create a daily crontab at 6:00 AM UTC
schedule = crontab(hour=6, minute=0)
now = datetime(2026, 1, 21, 21, 32, 36, tzinfo=timezone.utc) # 9:32 PM UTC
last_run_at = datetime(2026, 1, 21, 6, 0, 0, tzinfo=timezone.utc) # 6:00 AM UTC (ran today)
remaining = schedule.remaining_estimate(last_run_at)
print(f"remaining_estimate(last_run_at) = {remaining}") # ~8:27:23
# Current RedBeat calculation (WRONG)
due_at = last_run_at + remaining
print(f"RedBeat due_at = {due_at}") # 2026-01-21 14:27:23 (today afternoon!) ❌
# Correct calculation
correct_next_run = now + schedule.remaining_estimate(now)
print(f"Correct next_run = {correct_next_run}") # 2026-01-22 06:00:00 (tomorrow 6 AM) ✅
Output:
remaining_estimate(last_run_at) = 8:27:23.675409
RedBeat due_at = 2026-01-21T14:27:23.675409+00:00 ❌ Wrong! (today 2:27 PM)
Correct next_run = 2026-01-22T05:59:59.998661+00:00 ✅ Correct! (tomorrow 6 AM)
Expected Behavior
due_at should return the correct next scheduled run time. The fix should be:
@property
def due_at(self):
if self.last_run_at is None:
return self._default_now()
now = self._default_now()
delta = self.schedule.remaining_estimate(now) # Use 'now' instead of 'last_run_at'
if delta is None:
return None
# No need to check for negative delta when using 'now'
return now + delta
Or alternatively, keep the overdue check:
@property
def due_at(self):
if self.last_run_at is None:
return self._default_now()
now = self._default_now()
delta = self.schedule.remaining_estimate(now)
if delta is None:
return None
if delta.total_seconds() < 0:
return now
return now + delta
Impact
This bug causes due_at to return incorrect times, which can affect:
- UI displays showing "next scheduled run"
- Any code that reads
entry.due_at to determine when a task will run next
- Debugging and monitoring of scheduled tasks
Note: The actual task execution is likely not affected because RedBeat uses is_due() for scheduling decisions, not due_at. But due_at is exposed as a public property and users may rely on it.
Environment
- celery-redbeat version: 2.3.3
- celery version: 5.x
- Python version: 3.11
Workaround
Calculate next run time manually instead of using entry.due_at:
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
remaining = entry.schedule.remaining_estimate(now)
next_run = now + remaining
Description
The
due_atproperty inRedBeatSchedulerEntrycalculates an incorrect next run time. The issue is thatremaining_estimate()returns the time delta from now to the next scheduled time, butdue_atadds this delta tolast_run_atinstead ofnow.Current Behavior
In
redbeat/schedulers.py, thedue_atproperty is implemented as:The problem is that
remaining_estimate()returns the time until the next scheduled occurrence from now, regardless of what argument is passed. So adding this delta tolast_run_atproduces an incorrect result.Reproduction
Output:
Expected Behavior
due_atshould return the correct next scheduled run time. The fix should be:Or alternatively, keep the overdue check:
Impact
This bug causes
due_atto return incorrect times, which can affect:entry.due_atto determine when a task will run nextNote: The actual task execution is likely not affected because RedBeat uses
is_due()for scheduling decisions, notdue_at. Butdue_atis exposed as a public property and users may rely on it.Environment
Workaround
Calculate next run time manually instead of using
entry.due_at: