Migrating production infrastructure? Get up to ₹10,000 in migration credits. Apply now
voroaBlog
Key Value

Celery Redis Setup Guide: Broker, Results and Costs, Measured

· · 7 min read

Celery with Redis, measured: idle worker commands, commands per task and result memory

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

Advertisement

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.

Diagram: the Django or Flask app sends a task to a Redis list (the broker); a Celery worker takes it, runs it, and optionally writes the result to Redis (the result backend) where the app can read it

A minimal working setup

bash
pip install "celery[redis]"
python
# 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:

bash
celery -A tasks worker --loglevel=info --concurrency=4

And queue work from your app:

python
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

python
# 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 = True globally, and turn results on only for the tasks whose return value you actually fetch.
  • If you do keep results, lower result_expires to 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 eta longer 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

  1. Limited by memory, not commands. As measured above, idle workers send millions of commands a month.
  2. No eviction. If the store deletes keys when full, queued tasks can vanish. Use a policy that refuses writes instead; see Redis eviction policies.
  3. Data kept on disk. A store that empties on restart loses every queued task.
  4. 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.

Advertisement