Skip to content

Bug: due_at property calculates incorrect next run time due to misuse of remaining_estimate #307

Description

@jasonyaodd

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions