Celery beat crontab schedules validated and run eagerly: working versions (tested 2026-10-15)
Define beat_schedule crontab entries, validate task names and schedule types before deploy, and test each scheduled call offline with task_always_eager.
passed5 passed · 0 failed2026-10-15 13:27 UTC- Status
- passed
- Last passed
- 2026-10-15 13:27 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("celery-beat-schedule-crontab") (install), or see them on GitHub.
celery==5.6.3 pytest==9.1.1
from celery import Celery
from celery.schedules import crontab
app = Celery("tasks", broker="memory://", backend="cache+memory://")
@app.task
def prune_cache(max_age_hours: int) -> int:
# real code would delete stale rows and return how many were removed
return 0
@app.task
def send_report(recipients: list[str]) -> int:
return len(recipients)
app.conf.beat_schedule = {
"prune-cache-nightly": {
"task": "tasks.prune_cache",
"schedule": crontab(hour=3, minute=0),
"args": (24,),
},
"report-weekdays": {
"task": "tasks.send_report",
"schedule": crontab(minute="*/30", hour="9-17", day_of_week="mon-fri"),
"args": (["[email protected]"],),
},
}
def validate_beat_schedule(schedule: dict, known_tasks) -> None:
# beat only fails at runtime in the worker, so check names and types up front
for name, entry in schedule.items():
if entry["task"] not in known_tasks:
raise ValueError(f"{name}: unknown task {entry['task']!r}")
if not isinstance(entry["schedule"], crontab):
raise ValueError(f"{name}: schedule must be a crontab")
import pytest
from celery.schedules import crontab
from tasks import app, validate_beat_schedule
@pytest.fixture(autouse=True)
def eager():
app.conf.update(task_always_eager=True, task_eager_propagates=True)
yield
app.conf.update(task_always_eager=False)
def test_shipped_schedule_is_valid():
validate_beat_schedule(app.conf.beat_schedule, app.tasks)
def test_typo_in_task_name_is_caught():
bad = {"nightly": {"task": "tasks.prnue_cache", "schedule": crontab()}}
with pytest.raises(ValueError, match="unknown task"):
validate_beat_schedule(bad, app.tasks)
def test_crontab_strings_expand_to_sets():
c = crontab(minute="*/15", hour="9-11")
assert c.minute == {0, 15, 30, 45}
assert c.hour == {9, 10, 11}
def test_invalid_crontab_string_fails_fast():
with pytest.raises(ValueError):
crontab(minute="bogus")
def test_scheduled_entry_runs_eagerly():
entry = app.conf.beat_schedule["prune-cache-nightly"]
assert app.tasks[entry["task"]].apply_async(args=entry["args"]).get() == 0
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Celery 5 on Redis with fakeredis-backed idempotency, tested offline in eager mode
Configure a Celery app on a Redis broker and result backend for production, guard a task with a Redis SET NX idempotency lock, and test it offline with task_always_eager and fakeredis.
Celery 5 tasks, chains and groups tested in eager mode
Define Celery tasks, chain and fan them out with groups, then test them offline with task_always_eager and the in-memory broker.
Django post_save signal enqueuing a Celery task, tested with eager mode and on_commit
A post_save signal queues a Celery task only after the row commits via transaction.on_commit, then tests run offline on in-memory SQLite with Celery eager mode and pytest-django's on_commit capture.