Celery Redis Setup Guide: Broker, Results and Costs, Measured
Sagar Tayde · · 7 min read
A Celery Redis setup is one of the most common ways to run background tasks in Python: Celery sends tasks through Redis to worker processes, and can store their results there too. This guide is for Python and Django developers moving slow work, such as emails, reports and image processing, out of the request. It shows a working configuration, then what we measured about the Redis side that tutorials skip: how many commands an idle worker sends, what each task costs, and how much memory results keep.
On this page
- How Celery uses Redis
- A minimal working setup
- Django in six lines
- What an idle worker costs, measured
- What each task costs, measured
- Results: keep them only if you read them
- The visibility timeout trap
- Settings for production
- Choosing a Redis for Celery
- FAQ
How Celery uses Redis
Redis plays two separate roles, and you can use one or both:
- Broker. Your web app puts a task message on a Redis list. Workers take messages off the list and run them. Without a broker there is no Celery.
- Result backend. When a task finishes, Celery can store its return value and state in Redis so your app can read it later. This is optional, and often unnecessary.

A minimal working setup
pip install "celery[redis]"
# tasks.py
import os
from celery import Celery
app = Celery(
"tasks",
broker=os.environ["REDIS_URL"], # e.g. redis://:password@host:6379/0
backend=os.environ.get("RESULT_URL"), # optional; leave unset if you never read results
)
@app.task(bind=True, max_retries=5, default_retry_delay=10)
def send_welcome_email(self, user_id):
try:
deliver_email(user_id) # your code
except TemporaryError as exc:
raise self.retry(exc=exc)
Start a worker in its own process:
celery -A tasks worker --loglevel=info --concurrency=4
And queue work from your app:
send_welcome_email.delay(user.id)
For a TLS connection, use a rediss:// URL. For the result backend, add the certificate setting to the
URL, for example rediss://:password@host:6380/1?ssl_cert_reqs=required, or Celery refuses it.
Django in six lines
# settings.py
CELERY_BROKER_URL = os.environ["REDIS_URL"]
CELERY_TASK_IGNORE_RESULT = True
CELERY_TASK_ACKS_LATE = True
CELERY_TASK_REJECT_ON_WORKER_LOST = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
With the usual celery.py in your project (app.config_from_object("django.conf:settings", namespace="CELERY") and app.autodiscover_tasks()), any @shared_task in your apps is picked up.
Each setting above is explained below.
What an idle worker costs, measured
Setup: Celery 5.7.0, kombu 5.7.0 and redis-py 8.1.0 on Python 3.14, on a Windows laptop, using the
solo pool because Celery’s default pool does not run on Windows. Redis 8.10.3 ran in a Linux virtual
machine (WSL2) on the same laptop. We counted commands with Redis’s own command statistics. Measured 7
November 2026.
We started one worker with nothing to do, let it settle for 20 seconds, then counted for two minutes.
| One idle worker | Result |
|---|---|
| Commands in 2 minutes | 200 |
| Per minute | 100 |
| Per 30-day month | about 4.3 million |
An idle Celery worker polls the broker for new tasks and talks to other workers every few seconds. That is normal, and on a store that limits memory it costs nothing. On a host that charges per command, it is the bill: 4.3 million is more than eight times a 500,000-command free allowance, before a single task runs. The same is true of Node.js queues; our BullMQ measurements found about 110 commands a minute for an idle worker there.
What each task costs, measured
2,000 tasks, each add(x, y) |
Commands per task | Memory left in Redis per task |
|---|---|---|
| With a result backend, and the app reading each result | 18 | about 383 bytes |
With ignore_result=True |
about 10 | none |
The commands-per-task figures include the worker’s normal polling during the run, so treat them as upper bounds. Turning results off removed roughly 8 commands per task and all of the leftover memory.
Results: keep them only if you read them
By default, every result is kept in Redis for one day. We checked: a result key had a remaining lifetime of 86,396 seconds straight after the task finished.
At about 383 bytes each, 100,000 tasks a day keeps around 38 MB of results in memory at any time, which is nearly a third of a 128 MB store, holding values nobody reads. Most tasks, such as sending an email or resizing an image, do not need a stored result at all.
- Set
task_ignore_result = Trueglobally, and turn results on only for the tasks whose return value you actually fetch. - If you do keep results, lower
result_expiresto how long you really need them, for example one hour. - If you need to track progress for a user, store it in your database or publish it, rather than polling the result backend.
The visibility timeout trap
With Redis as the broker, a worker takes a task but Redis does not know whether it finished. Celery handles this with a visibility timeout, one hour by default: if a task has not been acknowledged within that time, it is given to another worker.
Two things follow:
- A task that runs longer than an hour can run twice. If you have long reports or exports, raise the
timeout above your longest task with
broker_transport_options = {"visibility_timeout": 7200}. - Tasks scheduled far in the future are redelivered. A task with a countdown or
etalonger than the visibility timeout can be handed out again before it is due. For anything scheduled hours ahead, use a scheduled task or Celery beat instead of a long countdown.
Either way, write tasks so that running twice is harmless: check whether the email was sent or the invoice created before doing it again.
Settings for production
| Setting | Value | Why |
|---|---|---|
task_ignore_result |
True |
Saves commands and memory unless you read results |
task_acks_late |
True |
Acknowledge after the task finishes rather than when it starts |
task_reject_on_worker_lost |
True |
With acks_late, requeue a task whose worker process was killed; without it, that task is acknowledged and lost. Watch for a task that always crashes its process, which then retries forever |
worker_prefetch_multiplier |
1 |
Each worker process reserves one task at a time (a worker with concurrency 4 reserves 4), so long tasks do not hold others hostage |
broker_transport_options |
{"visibility_timeout": ...} |
Longer than your slowest task |
task_time_limit |
e.g. 600 |
Kill a task that hangs instead of blocking a worker forever |
broker_connection_retry_on_startup |
True |
Keep retrying if Redis is not reachable when the worker boots |
Even with both settings, if the whole worker machine or container dies, its unacknowledged tasks come back only after the visibility timeout, up to an hour by default.
Run the worker as its own long-running process, not inside your web server. On Voroa that is a background worker, which never sleeps; the steps are in deploying a background worker, and the Redis URL goes in an environment variable.
Choosing a Redis for Celery
- Limited by memory, not commands. As measured above, idle workers send millions of commands a month.
- No eviction. If the store deletes keys when full, queued tasks can vanish. Use a policy that refuses writes instead; see Redis eviction policies.
- Data kept on disk. A store that empties on restart loses every queued task.
- The same region as the worker. Every task fetch is a round trip; a store in another country slows every one.
Celery should work the same on Redis and Valkey, since it uses core list and key commands; test with your own tasks before you switch. our Valkey vs Redis comparison covers where the two differ. On Voroa, a key value store defaults to Keep everything when memory is full, keeps data on disk with a nightly snapshot, and runs in Mumbai beside your workers. See the Key Value guide.
Run Celery and its Redis in one place
Your Django or Flask app, the Celery worker and a Redis-compatible store, side by side in Mumbai, with a free store size to start. See the sizes on the managed Redis page.
FAQ
Do I need a result backend for Celery?
No. Only if your app reads task return values. Without one, tasks still run; set
task_ignore_result = True.
Why is my Redis memory growing with Celery?
Usually stored results, which are kept for a day by default. Ignore results you do not read, or lower
result_expires.
Why did my Celery task run twice? With Redis as the broker, a task not acknowledged within the visibility timeout (one hour by default) is redelivered. Long tasks and long countdowns are the usual cause.
Can I use the same Redis for the broker and results? Yes, ideally on separate database numbers or key prefixes. On a small store, keep results off so they do not compete with the queue for memory.
Does Celery work on Windows?
For development, with --pool=solo. Run production workers on Linux.
Is Redis or RabbitMQ better for Celery? Redis is simpler to run and fine for most apps. RabbitMQ gives stronger delivery guarantees without a visibility timeout and suits very high volumes or complex routing.
Measured 7 November 2026 with Celery 5.7.0 and Redis 8.10.3. Found something out of date? Write to support@getvoroa.com.