celery beat vs celery worker (tested)
celery beat only sends scheduled tasks; celery worker runs them. You need both: a worker alone ran our 2-second periodic task 0 times. Run one celery -A proj beat plus your workers, or worker -B for development.
celery beat is a scheduler: it reads beat_schedule and, when an entry is due, sends a task message to the broker. A celery worker consumes messages and runs tasks. Beat never runs tasks itself, and a worker never looks at beat_schedule. Measured with Celery 5.6.3 and Redis 7, a 2-second schedule, 10 seconds per run.
1. Worker alone: periodic tasks never run
$ cat celery_app.py
from celery import Celery
app = Celery("proj", broker="redis://127.0.0.1:8964/0")
app.conf.beat_schedule = {
"ping-every-2s": {"task": "celery_app.ping", "schedule": 2.0},
}
@app.task
def ping():
print("ping ran")
$ celery -A celery_app worker -c 1 --loglevel=INFO > worker.log 2>&1 &
$ echo "ping ran: $(grep -c "ping ran" worker.log) times in 10s"
ping ran: 0 times in 10s
The schedule is defined and the task is registered, but nothing sends the messages, so it ran 0 times and the worker logged no error. This is the usual cause of "celery beat not running tasks".
2. Worker with -B: beat embedded in the worker
$ celery -A celery_app worker -B -c 1 --loglevel=INFO > worker.log 2>&1 &
$ echo "ping ran: $(grep -c "ping ran" worker.log) times in 10s"
ping ran: 4 times in 10s
$ grep -m2 -E "beat: Starting|Sending due task" worker.log | sed -E "s/^\[[^]]*\] //"
beat: Starting...
Scheduler: Sending due task ping-every-2s (celery_app.ping)
-B starts a beat thread inside the worker. Convenient for development; Celery's documentation advises against it in production, because every worker started with -B runs its own scheduler and you must never have more than one beat sending the same schedule.
3. Separate beat process (production setup)
$ celery -A celery_app worker -c 1 --loglevel=INFO > worker.log 2>&1 &
$ celery -A celery_app beat --loglevel=INFO > beat.log 2>&1 &
$ echo "ping ran: $(grep -c "ping ran" worker.log) times in 10s"
ping ran: 6 times in 10s
$ grep -m2 "Sending due task" beat.log | sed -E "s/^\[[^]]*\] //"
Scheduler: Sending due task ping-every-2s (celery_app.ping)
Scheduler: Sending due task ping-every-2s (celery_app.ping)
Run exactly one celery beat process and as many workers as you need. Beat stores its last-run times in celerybeat-schedule in the working directory (change with -s), so give it a writable directory.
Related: Celery Received unregistered task (beat sends the task name; the worker must have the task registered), Celery task time limits.
Frequently asked questions
What is the difference between celery beat and celery worker?
Beat is a scheduler that publishes due tasks from beat_schedule to the broker; the worker consumes and executes tasks. Periodic tasks need both.
Why are my Celery periodic tasks not running?
Usually no beat process is running. A worker alone ignores beat_schedule. Start celery -A proj beat or add -B to one worker.
Can I run celery beat inside the worker?
Yes, with worker -B, but only on one worker; it is meant for development.
Written with AI assistance. Measured on 2026-10-23 with Celery 5.6.3 (kombu 5.6.2, redis-py 8.1.0) on Python 3.13.5, Redis 7 (Docker) as broker. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Celery and Redis are free.