1.The Celery app, tasks, workers, and task discovery
A Celery deployment has three pieces. The **app** is a `Celery(...)` instance created once in `config/celery.py` and imported from `__init__.py` so it exists before any task is defined. A **task** is a function registered with that app — in a Django project you use `@shared_task`, which registers with whichever app is configured rather than binding to one instance, so reusable apps work. A **worker** is a separate process (`celery -A config worker`) that consumes messages and runs those functions; it is not part of your web server and does not share its memory. **Task discovery** is `app.autodiscover_tasks()`, which imports `tasks.py` from every entry in `INSTALLED_APPS` — which is why a task in a file with any other name is invisible.
Key features
- The app object lives in `config/celery.py` and is imported from `config/__init__.py`, so it is created before Django loads any app module
- `@shared_task` registers with the current app rather than a specific instance, which is what makes tasks in reusable apps work — prefer it in a Django project
- `.delay(*args)` is shorthand for `.apply_async(args=...)`; both publish a message and return an `AsyncResult` immediately, without running anything
- `app.autodiscover_tasks()` imports `tasks.py` from every `INSTALLED_APPS` entry — a task defined anywhere else is never registered in the worker
- A worker is a separate process with its own imports, settings, database connections and memory; nothing is shared with the web process
- `celery -A config worker -l info` prints the registered task list at startup — the fastest way to confirm discovery actually found your task
The three pieces, and where each one lives
- View calls send_email.delay(57) — returns in microseconds — nothing has run
- leads to Serialize name + args
- Serialize name + args — JSON by default, so arguments must be JSON-safe
- leads to Broker (Redis / RabbitMQ)
- Broker (Redis / RabbitMQ) — the message waits here — possibly across a deploy
- leads to Worker process
- Worker process — separate imports, separate settings, separate connections
- leads to Task registry lookup by name
- Task registry lookup by name — populated by autodiscover_tasks() from tasks.py only
- on error, leads to NotRegistered (name not found)
- leads to The function body finally runs (found)
- The function body finally runs
- leads to Result backend (optional) (if a backend is configured)
- NotRegistered — the task exists in the web process and not in the worker
- Result backend (optional) — only if configured — otherwise the return value is discarded
Command
@shared_task
def send_welcome_email(user_id):
...
send_welcome_email.delay(user.pk) # publishes a message; does not run anythingThe app is created in `config/celery.py` and imported from `config/__init__.py` so it exists before any task is defined. `@shared_task` registers with the current app, which is what makes reusable apps work. `.delay()` publishes a message and returns — the body runs later, in a separate process with its own imports, settings and connections, so pass ids and read current state. And autodiscovery imports `tasks.py` specifically: a task defined anywhere else works in your web process and raises `NotRegistered` in the worker.