skip to content

A Celery worker busy with long report tasks holds short webhook tasks while another worker sits idle; how do worker_prefetch_multiplier and -O fair explain it?

level: seniorimportance: must knowfreq 40%

answer

  1. reserved is not available
  2. slots times a multiplier
  3. fair is already the default
  4. inside one worker, not across
  5. 5.6 fetch-on-free-slot option

basics

~20 s

Each Celery worker reserves concurrency × worker_prefetch_multiplier messages (4 per slot by default), so webhooks wait in a busy worker's buffer. -O fair, the default since 4.0, only avoids busy children inside one worker; a lower prefetch fixes it.

solid answer

~40 s

A worker asks the broker for up to `worker_concurrency × worker_prefetch_multiplier` unacknowledged messages; with `-c 4` and the default multiplier 4 that is 16. Reserved messages sit in that worker's memory, and while they are reserved no other worker can take them. If four long reports occupy all four children, the twelve webhook tasks behind them wait while an idle worker has nothing. `-O fair` does not help: since 4.0 the `default` profile already maps to the fair strategy, which only stops a prefork worker handing a task to a child that is busy. The fixes are `worker_prefetch_multiplier = 1` (one extra reserved per slot with early acks, none extra with `task_acks_late`), Celery 5.6's `worker_disable_prefetch` (Redis broker only), or better, separate queues and workers for long and short tasks.

code

python · 12 lines
python
from celery import Celery

app = Celery('proj', broker='redis://localhost:6379/0')

# report workers: reserve at most one extra message per child
app.conf.worker_prefetch_multiplier = 1

# Celery 5.6, Redis broker only: fetch only when a slot is free
app.conf.worker_disable_prefetch = True

# Celery 5.6: cap countdown/ETA tasks held in memory
app.conf.worker_eta_task_limit = 1000

go deeper

for a junior

Recall that a Celery worker reserves messages ahead of running them, and that the default prefetch multiplier is 4 per slot.

for a middle

Explain the concurrency × multiplier arithmetic, why reserved messages are invisible to other workers, and why a multiplier of 0 means unlimited.

for a senior

Diagnose starvation with inspect reserved, explain why -O fair is irrelevant on 5.6, and choose between multiplier 1, acks_late, worker_disable_prefetch and split queues.

for a principal

Decide per workload whether throughput from prefetching is worth the latency risk, and make queue separation the default design for mixed run times.

## What prefetch means in Celery **Prefetch** is the number of messages a worker may reserve from the broker before it has a free slot to run them. The broker delivers messages up to that limit; they stay **unacknowledged** and live in the worker's memory as **reserved** tasks. Until the worker acknowledges them or its connection drops (or, on Redis and SQS, their visibility timeout expires), no other worker can receive them. Celery computes the limit as: - `worker_concurrency × worker_prefetch_multiplier`; - `worker_prefetch_multiplier` defaults to **4**; - a multiplier of **0** removes the limit entirely — the worker keeps consuming as many messages as it wants. Prefetching exists for throughput: a worker running thousands of 20 ms tasks should not pay a broker round-trip before each one. ## How it starves short tasks Take two workers, A and B, each with `-c 4` on the `prefork` pool, both consuming one queue that mixes 10-minute report renders with 200 ms webhook calls. | Moment | Worker A | Worker B | |---|---|---| | A starts first | reserves 16 messages: 4 reports, 12 webhooks | not yet connected | | A's 4 children run reports | 12 webhooks wait in A's buffer | idle, queue is empty | | 10 minutes later | webhooks finally run | still idle | The configuration docs warn about exactly this: the first worker to start receives four times its concurrency in messages, so long tasks are not fairly distributed. ## What -O fair actually does `-O` / `--optimization` accepts `default` or `fair`. In the prefork pool the `default` profile **maps to the fair scheduling strategy**, and has done since Celery 4.0. The fair strategy is about dispatch **inside one worker**: the parent process will not write a task into the pipe of a child that is already executing one, so a short task is not stuck behind a long one when another child of the same worker is free. It does nothing about messages **reserved across workers**. Advice to "add `-Ofair`" predates 4.0; on 5.6 it changes nothing. ## The fixes and what each costs | Setting | Effect | Cost | |---|---|---| | `worker_prefetch_multiplier = 1` | at most one extra reserved message per slot (early acks) | more broker round-trips for short tasks | | multiplier 1 + `task_acks_late = True` | reserves only as many as there are slots | tasks must be idempotent; a killed worker's task reruns | | `worker_disable_prefetch = True` (5.6) | fetches only when a slot is free, keeps early acks | Redis broker only; other transports ignore it with a warning | | separate queues and workers | long and short tasks never share a buffer | a second deployment to run | The optimizing guide's own recommendation for a mix of long and short tasks is the last row: two worker nodes configured separately, with tasks routed by run time. The report workers then run multiplier 1; the webhook workers can keep a larger multiplier because their tasks are short. ## ETA tasks slip past the window Tasks sent with `countdown` or `eta` are received immediately and held on the worker's internal timer until due. The worker raises its prefetch allowance for each one so they do not block normal work, which is why `--prefetch-multiplier=1` can appear to have no effect when many retried webhooks carry countdowns. Celery **5.6** adds `worker_eta_task_limit` (default `None`, no limit) to cap how many ETA tasks a worker holds; once the cap is reached it stops consuming until some run. ## Confirming it in production 1. Run `celery -A proj inspect reserved` and look for many webhook tasks reserved on a worker whose slots are all busy. 2. Compare with `inspect active` on the idle workers. 3. Check the startup banner or `app.conf` for the effective multiplier and concurrency. ## Why raising concurrency is not the fix A tempting response is to add slots to the busy worker. On `prefork`, that also multiplies the reservation: going from `-c 4` to `-c 8` with the default multiplier raises the prefetch window from 16 to 32, so the worker hoards more webhooks behind more reports, and every extra child costs a full process's memory. The starvation comes from **mixing run times in one buffer**, not from too few slots, so the lasting fixes are the ones above: - shrink the buffer for long tasks (multiplier 1, or no prefetch); - or stop long and short tasks from sharing a buffer at all (separate queues and workers).

  • Why can --prefetch-multiplier=1 appear to do nothing when a Celery queue holds many countdown tasks?
    ETA and countdown tasks are delivered at once and wait on the worker's timer; the worker raises its prefetch allowance for each so they do not block other work. A worker can therefore hold thousands. Celery 5.6's `worker_eta_task_limit` caps how many it keeps; the default is no limit.
  • Would setting Celery's worker_prefetch_multiplier to 0 disable prefetching?
    No, the opposite. A multiplier of 0 removes the limit, so the worker keeps consuming as many messages as it wants — the worst setting for fairness and memory. To stop prefetching use a multiplier of 1 with `task_acks_late`, or `worker_disable_prefetch` on a Redis broker.
  • Why is splitting reports and webhooks onto separate Celery workers better than tuning one?
    Any shared buffer lets long tasks hold short ones, and one prefetch value cannot suit both. Separate workers let the report side run multiplier 1 on prefork while the webhook side keeps a larger multiplier on gevent for throughput. It is the optimizing guide's recommendation for mixed run times.

saying these in an interview costs you the question

  • Adding -O fair on Celery 5.6 fixes starvation across workers.
  • worker_prefetch_multiplier = 0 turns prefetching off.
  • With multiplier 1 and early acks, a worker holds no queued tasks.
  • worker_disable_prefetch works on every broker, RabbitMQ included.
  • Messages a worker has prefetched stay available to idle workers.