Nightly reports, cache warmups and cleanup jobs are periodic tasks, and Celery replaces cron without extra infrastructure.
celery beat is a separate scheduler process. It doesn't execute tasks; it sends them to the broker on schedule, and normal workers run them.
Define schedules in app.conf.beat_schedule, a dict of entries:
"task": the registered task name."schedule": seconds (30.0), atimedelta, or acrontab(...)."args"/"kwargs"and optional"options"(same as apply_async, e.g. a queue).
crontab(minute=0, hour=3) is daily at 03:00; crontab(minute="*/15") every 15 minutes; crontab(hour=7, minute=30, day_of_week="mon-fri") weekdays at 7:30. Times use the app's timezone setting.
Start it with celery -A proj beat -l INFO. In development you can embed it in a worker with celery -A proj worker -B, but not in production. Beat keeps its last-run times in a local celerybeat-schedule file by default; django-celery-beat stores schedules in the database and lets admins edit them.
The crucial rule: run exactly one beat process. Two schedulers send every task twice. In containers, beat gets its own single-replica deployment.
If a job must not overlap with its previous run (long reports), add a lock in the task, e.g. a Redis SET NX with expiry, because beat doesn't check whether the last run finished.
Example
# proj/celery.py
from celery import Celery
from celery.schedules import crontab
app = Celery("proj", broker="redis://localhost:6379/0")
app.conf.timezone = "UTC"
app.conf.beat_schedule = {
"cleanup-sessions-every-15-min": {
"task": "proj.tasks.cleanup_sessions",
"schedule": crontab(minute="*/15"),
},
"nightly-report": {
"task": "proj.tasks.build_report",
"schedule": crontab(hour=3, minute=0),
"args": ("daily",),
"options": {"queue": "reports"},
},
"heartbeat": {"task": "proj.tasks.ping", "schedule": 30.0},
}
# $ celery -A proj beat -l INFO (one instance only)
# $ celery -A proj worker -l INFO -Q celery,reportsRemember
- beat only sends tasks on schedule; workers execute them
- Schedules: seconds, timedelta or crontab(...) in the app timezone
- Run exactly one beat instance in production
- Use a lock if runs must not overlap
Watch out for
- Running beat inside every worker replica with -B, so each job runs once per replica.