Django Seedkit

Blog · Background work

How to add Celery to Django with Redis

Django has no background-task system. Celery adds workers, retries, rate limits, and schedules. With Redis as the broker, the setup is one package and two files.

uv add 'celery[redis]'

Celery needs an app object that loads the Django settings. Put the object beside the settings module. Import it from the package __init__.py, so that it exists before Celery imports a task module.

# config/celery.py
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")

app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()
# config/__init__.py
from .celery import app as celery_app

__all__ = ("celery_app",)

Set the default settings module to production, not local. A worker with no DJANGO_SETTINGS_MODULE in its environment then starts with the production settings. The development shell names config.settings.local to override the default.

Settings

The CELERY namespace means that Celery reads every setting below from the Django settings object. Give the broker and the result backend separate Redis database numbers. Record which number belongs to which service, because the cache, a channel layer, and a rate limiter each need one.

REDIS_URL = env("REDIS_URL", default="redis://127.0.0.1:6379").rstrip("/")

CELERY_BROKER_URL = f"{REDIS_URL}/1"
CELERY_RESULT_BACKEND = f"{REDIS_URL}/2"
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
CELERY_TASK_SOFT_TIME_LIMIT = 540
CELERY_TASK_TIME_LIMIT = 600

The .rstrip("/") call matters when a managed provider supplies the URL. A slash at the end builds redis://host:6379//1, which is not the correct URL.

A task with no time limit can hold a worker slot forever. Celery sets no limit by default. A task that waits on a socket read keeps its slot, and enough of these tasks stop the queue. Nothing fails, so nothing reaches the log. Set CELERY_TASK_SOFT_TIME_LIMIT: it raises SoftTimeLimitExceeded inside the task, and the task can then clean up. CELERY_TASK_TIME_LIMIT kills the process if the task ignores the soft limit.

Run the worker in a second terminal, with Redis already up.

DJANGO_SETTINGS_MODULE=config.settings.local uv run celery -A config worker -l info

Or give the task to an agent. It reads the same setup from a maintained reference file and applies it to the repository:

/django-seedkit add Celery background tasks

The reference holds the full setup, the Compose service, the healthcheck, and Celery Beat: tasks-celery.md · What Django Seedkit is