/
Application Framework

Celery Beat Schedule Guide

Celery Beat is the scheduler component for Celery, Python's most widely used distributed task queue — it periodically kicks off tasks according to a schedule defined in code rather than a separate crontab file, using either simple interval schedules or a crontab()-style helper that mirrors standard cron fields.

Syntax Notes

Celery's crontab() helper takes keyword arguments (minute=, hour=, day_of_week=, day_of_month=, month_of_year=) that each accept the same wildcards, ranges, steps, and lists as standard cron fields — just expressed as Python keyword arguments instead of a single 5-field string.

Examples

Run a task every day at 2:30 AM
from celery.schedules import crontab

app.conf.beat_schedule = {
    'nightly-cleanup': {
        'task': 'myapp.tasks.cleanup',
        'schedule': crontab(hour=2, minute=30),
    },
}
Run every 15 minutes
'schedule': crontab(minute='*/15')
Run every weekday at 9 AM
'schedule': crontab(hour=9, minute=0, day_of_week='1-5')

Gotchas

  • Celery Beat must run as its own separate process (celery -A myapp beat) alongside the worker process — a common mistake is running only celery worker and wondering why scheduled tasks never fire, since the worker executes tasks but never schedules them.
  • Running more than one Beat instance simultaneously schedules every task multiple times — Beat is a singleton scheduler by design, so use a single Beat process (or a distributed lock via django-celery-beat's database scheduler) rather than one Beat per worker replica.
  • crontab(minute='*/15') has the exact same step-value gotcha as plain cron — it only fires at :00, :15, :30, :45, not truly every 15 minutes from whenever the app started.

Best Practices

  • Deploy exactly one Beat process per environment, separate from your worker processes — scaling out worker replicas should never mean scaling out Beat.
  • For dynamic, database-editable schedules (rather than ones baked into code that require a redeploy to change), use the django-celery-beat package's DatabaseScheduler instead of the default file-based one.
  • Double-check timezone handling — Celery's timezone defaults to UTC unless CELERY_TIMEZONE (or app.conf.timezone) is explicitly set, a common source of schedules firing at unexpected local times.

Frequently Asked Questions

Why isn't my Celery Beat schedule running at all?
The most common cause is not running a Beat process — celery worker alone executes tasks but never schedules them. You need a separate `celery -A myapp beat` process (or `celery -A myapp worker -B` to run both in one process, fine for development but not recommended in production).
Can Celery Beat schedules be edited without redeploying code?
Not with the default file-based scheduler, since beat_schedule is defined in application code. Use the django-celery-beat package, which stores schedules in the database and lets you edit them via Django admin or the ORM without a redeploy.
Does crontab(minute='*/15') in Celery behave differently from cron's */15?
No — it has the exact same semantics as cron: it fires at fixed clock positions (:00, :15, :30, :45), not on a rolling 15-minute timer from whenever the app started or was last restarted.