In a Django project using Celery, why do apps define tasks with @shared_task instead of @app.task, and how are they discovered?
answer
- reusable apps cannot import the project
- a proxy to the current app
- a module named tasks
- discovery is lazy by default
basics
~20 s@shared_task creates a task without importing a concrete Celery app, so a Django app never imports the project's celery module; autodiscover_tasks() then imports each installed app's tasks.py so the task is registered with the project's app.
solid answer
~40 s`@app.task` needs the app object, so `orders/tasks.py` would have to import `shop.celery`, tying a reusable Django app to one project and inviting circular imports. `@shared_task` from `celery` returns a proxy that registers the task with whichever Celery app is current, including apps created later, so the Django app stays independent. Registration still needs the module to be imported: `app.autodiscover_tasks()` asks Celery's Django integration for the installed apps and imports `<app>.tasks` from each. By default that happens lazily when the worker imports its modules, and `related_name` changes the module name. A task in `orders/jobs.py` is not found unless something imports it, and the worker then rejects messages for it as an unregistered task.
code
python · 9 linesfrom celery import shared_task
from .models import Order
@shared_task(name="orders.send_confirmation")
def send_order_confirmation(order_id):
order = Order.objects.get(pk=order_id)
order.send_confirmation_email()go deeper
Recall that Django apps use @shared_task in their tasks.py and that autodiscover_tasks() in celery.py imports those modules.
Explain why @shared_task avoids importing the project package, how autodiscovery builds its list from installed apps, and related_name and force.
Diagnose unregistered tasks by checking imports, INSTALLED_APPS and worker targets, and protect queued messages with stable task names.
Decide how task modules and names are organised across apps so they stay stable as teams refactor and deploy independently.
## The problem @shared_task solves A **Celery app** (the `Celery(...)` instance in `shop/celery.py`) holds a **task registry**: the mapping from task names to functions that a worker uses to run incoming messages. The obvious way to register a task is `@app.task`: ```python from shop.celery import app @app.task def send_order_confirmation(order_id): ... ``` That works, but the `orders` app now imports the **project** package. Django apps are meant to be reusable across projects, and the project package already imports the apps (through `INSTALLED_APPS` and URLconfs), so this also invites circular imports. **`@shared_task`** removes the dependency: ```python from celery import shared_task @shared_task def send_order_confirmation(order_id): ... ``` It returns a **proxy** that always resolves to the task in the **current app's** registry. Celery arranges for every app — already finalised or created later — to register a copy of the function. The Celery docs describe it as the way to create tasks "without having any concrete app instance", aimed at reusable apps. This is also why the project's `__init__.py` imports the Celery app: it makes your configured app the current one before views start importing shared tasks. ## How discovery works A decorator only runs when its module is imported. Something must import `orders/tasks.py` in the worker, or the task is never registered there. That is `autodiscover_tasks()`: | Call | What gets imported | |---|---| | `app.autodiscover_tasks()` | `<app>.tasks` for every installed Django app, via Celery's Django fixup | | `app.autodiscover_tasks(["orders", "billing"])` | `orders.tasks`, `billing.tasks` only | | `app.autodiscover_tasks(related_name="jobs")` | `<app>.jobs` instead of `<app>.tasks` | | `app.autodiscover_tasks(force=True)` | The same, but immediately instead of lazily | Points worth knowing: - With no packages, the list comes from Django's app registry — every entry in `INSTALLED_APPS`, including third-party apps that ship `tasks.py`. - Discovery is **lazy** by default: it is connected to Celery's module-import step, which the worker runs at startup, and at that step Celery's Django fixup calls `django.setup()` first, so models are importable inside `tasks.py`. - A task in a module not named `tasks.py` is not found unless it is imported from `tasks.py`, listed explicitly, or the module name is changed with `related_name`. ## The symptom when discovery misses a task The web process can still queue the task: `send_order_confirmation.delay(order.pk)` only needs the task name. The worker receives the message, finds no such name in its registry, and logs that it received an **unregistered task**; the order confirmation is never sent. The usual causes: 1. The task lives in `orders/emails.py`, not `orders/tasks.py`. 2. The app is missing from `INSTALLED_APPS` in the settings the worker uses. 3. The worker was started with a different `-A` target or settings module. 4. The task was renamed and old messages carry the old name. ## Names Unless `name=` is given, the task name is the function's module path, for example `orders.tasks.send_order_confirmation`. Messages carry that name, so moving the function to another module changes the name, and messages already queued under the old name become unregistered after a deploy. For long-lived tasks, an explicit `@shared_task(name="orders.send_confirmation")` decouples the name from the file layout. ## Checklist - Tasks in each app's `tasks.py`, decorated with `@shared_task`. - `app.autodiscover_tasks()` in `celery.py`. - Celery app imported in the project package's `__init__.py`. - Stable names for tasks that may sit in the queue across deploys.
- The web process queues a task fine, but the worker logs an unregistered task. What do you check first?Whether the worker imported the module that defines it: is the function in the app's `tasks.py`, is the app in the worker's `INSTALLED_APPS`, and is the worker started with the right `-A` target and settings module. Queuing only needs the name, so the web side can succeed while the worker's registry lacks the task.
- Why can moving a task function to another module break production during a deploy?The default task name is the module path plus function name, and queued messages carry that name. After the move, the new workers register a different name, so messages queued before the deploy become unregistered. Setting an explicit `name=` on the decorator avoids this.
@shared_task is a job advert pinned to a shared noticeboard instead of posted to one company's inbox; whichever company runs the office picks it up, but only if someone actually walks past the board, which is what autodiscovery does.
saying these in an interview costs you the question
- Imports the project's Celery app inside every Django app's tasks.py
- Assumes autodiscover_tasks finds tasks in any module of the app
- Believes @shared_task makes the task run in every worker at once
- Thinks a task that queues fine from the web process must be registered in the worker
- Renames or moves task functions without considering queued messages