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.