Fix Celery Beat Not Sending Scheduled Tasks
Fix Celery Beat that runs but never sends periodic tasks in Django: check that Beat is actually running, the scheduler and timezone settings, django-celery-beat database schedules, stale schedule files, and queue routing.
Problem statement
Workers process tasks you call from views, but nightly cleanups, hourly syncs, and reminder emails never happen. Either Beat is not running at all, or it runs and its log shows no Scheduler: Sending due task lines, or it sends them and nothing consumes them.
Beat is a separate process whose only job is to put messages on the broker at the right times. A worker never runs periodic tasks by itself. So a periodic task can fail at three points: Beat not running, Beat not considering the task due, or the message going to a queue no worker listens on.
- Confirm a Beat process is running:
systemctl status celery-beatordocker compose ps beat. - Read its log for
Sending due tasklines at-l info. - If you use
django-celery-beat, start Beat with--scheduler django_celery_beat.schedulers:DatabaseSchedulerand check the periodic task is enabled. - Check
CELERY_TIMEZONEandUSE_TZ; crontab times are interpreted in Celery's timezone. - Delete a stale
celerybeat-schedulefile after changing schedules or timezones. - Make sure the task's queue has a worker:
celery -A config inspect active_queues.
Step-by-step solution
1. Make sure Beat runs, exactly once
Beat must be its own supervised process:
celery -A config beat -l info
In systemd use a dedicated celery-beat.service; in Compose a separate beat service; in Kubernetes a Deployment with replicas: 1. Running the worker with -B embeds Beat, which is fine for development but means every worker replica runs its own Beat, so duplicates appear when you scale. Run one Beat only; see Fix Celery Beat Duplicate Tasks.
2. Read the Beat log
A healthy Beat logs every send:
[INFO/MainProcess] beat: Starting...
[INFO/MainProcess] Scheduler: Sending due task cleanup-sessions (accounts.tasks.cleanup)
If you see beat: Starting... and nothing else, Beat does not consider any task due. If you see Sending due task but the task never runs, the problem is downstream at the queue or worker.
3. Use the scheduler that holds your schedule
There are two schedulers and they do not share data:
- Default
PersistentScheduler: readsCELERY_BEAT_SCHEDULEfrom settings and keeps state in a localcelerybeat-schedulefile. django_celery_beatDatabaseScheduler: reads periodic tasks from the database tables you edit in the admin.
If you created tasks in the Django admin but Beat runs the default scheduler, it never sees them. Start it with:
celery -A config beat -l info \
--scheduler django_celery_beat.schedulers:DatabaseScheduler
or set CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler". In the admin, check the task is Enabled, has a schedule attached, and that Task (registered) points at a name that still exists.
4. Check timezones
Crontab schedules are evaluated in CELERY_TIMEZONE (default UTC). A task set for hour=2 runs at 02:00 UTC, which may look like "never" if you are watching at 02:00 local time:
TIME_ZONE = "Europe/Berlin"
USE_TZ = True
CELERY_TIMEZONE = TIME_ZONE
CELERY_ENABLE_UTC = True
With django_celery_beat, each crontab schedule also has its own timezone field. After changing timezone settings, run python manage.py shell -c "from django_celery_beat.models import PeriodicTask; PeriodicTask.objects.update(last_run_at=None)" so Beat recalculates due times.
5. Remove a stale schedule file
The default scheduler stores last_run_at per task in celerybeat-schedule. After renaming tasks, changing intervals, or switching timezone, that file can hold values that keep tasks "not due". Stop Beat, delete the file, start Beat:
sudo systemctl stop celery-beat
rm -f /srv/app/celerybeat-schedule*
sudo systemctl start celery-beat
Point the file at a writable path with --schedule /var/lib/celery/beat-schedule. In a read-only container, Beat fails to write it and may exit; give it a volume or use the database scheduler.
6. Check routing and workers
If Beat sends but nothing runs, find the queue:
celery -A config inspect active_queues
A task routed to reports via CELERY_TASK_ROUTES or options={"queue": "reports"} in the schedule needs a worker started with -Q reports. Otherwise the messages pile up in Redis unconsumed.
Verification check: temporarily add a test schedule every minute, for example {"task": "core.tasks.ping", "schedule": 60.0}, and watch for the Sending due task line in Beat and the succeeded line in a worker within two minutes. Remove it afterwards.
Explanation
Beat keeps an in-memory heap of tasks ordered by the next due time, wakes up when the earliest is due, publishes a message, and computes the next time. It never executes code itself. Because it is a single quiet process with no traffic, it is easy to forget in deploy configs, and because it only logs on sends, a misconfigured Beat looks idle rather than broken. Monitoring the result of scheduled jobs, not just the process, is what catches this.
Edge cases / notes
- Beat crashes on startup with a database error if
django_celery_beatmigrations have not run. Runmigratebefore starting Beat in deploys. - Interval tasks after downtime. If Beat was down for a day, it runs a missed interval task once at startup, not once per missed interval.
- Solar schedules and DST. Crontab schedules around a DST change can skip or repeat an hour in local timezones. Use UTC for critical jobs.
- Monitor the outcome. Add a heartbeat check for important jobs so a silent Beat is noticed within one interval.
Next steps
- Set Beat up from scratch with Set Up Celery Beat for Django in Production.
- See task throughput and queues in Monitor Celery With Flower.
FAQ
Do Celery workers run periodic tasks on their own?
No. A separate Beat process sends them as messages; workers then run them like any other task.
Can I run Beat inside the worker with -B?
For development. In production, run one dedicated Beat so scaling workers does not multiply schedules.
Why do admin changes not take effect?
Beat is probably using the default scheduler. Start it with the DatabaseScheduler to read schedules from the admin.
Where is celerybeat-schedule stored?
In Beat's working directory unless you pass --schedule. It must be writable.