skip to content

Signals, Tasks & Email

Django moves work off the view path with signals, the django.tasks API, Celery wiring and outgoing mail. Interviewers ask what still runs in the request and what survives a crash.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

21

How do you wire Celery into a Django project, and what goes into celery.py and the project package's __init__.py?

level: juniorimportance: must knowfreq 58%

answer

  1. one app instance per project
  2. settings module before the app
  3. configuration read from Django settings
  4. discover each app's tasks module

basics

~10 s

Create proj/celery.py that sets DJANGO_SETTINGS_MODULE, builds Celery('proj'), calls config_from_object('django.conf:settings', namespace='CELERY') and autodiscover_tasks(); then import that app in proj/init.py so it loads whenever Django starts.

solid answer

~30 s

Celery documents a small, fixed recipe. In the project package next to `settings.py` I add `celery.py`: first `os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")`, then `app = Celery("shop")`, then `app.config_from_object("django.conf:settings", namespace="CELERY")` so all Celery options live in `settings.py` as `CELERY_*` names, then `app.autodiscover_tasks()` so every installed app's `tasks.py` is imported. In `shop/__init__.py` I add `from .celery import app as celery_app` so the app exists whenever Django starts and `@shared_task` functions bind to it. Tasks live in each app's `tasks.py`, and the worker runs as its own process with `celery -A shop worker -l INFO`.

code

python · 9 lines
python
import os

from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings")

app = Celery("shop")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

go deeper

for a junior

Recall the two files: celery.py with the four setup lines, and the import in the project package's init.py, plus a worker started as its own process.

for a middle

Explain why the settings module must be set first, what the CELERY namespace does to setting names, and how autodiscover_tasks finds each app's tasks module.

for a senior

Explain what the Django fixup does in the worker, such as django.setup() and closing database connections around tasks, and diagnose a worker that cannot load models.

for a principal

Decide whether a project standardises on Celery or a backend behind Django's own task API, and who owns worker deployment and configuration.

## What "wiring" means **Celery** is a separate task-queue library with its own worker processes. It needs no Django-specific package any more; you connect it to a Django project with a few lines of configuration so that: - the worker process can load Django (settings, models, the ORM), - Celery reads its options from Django's `settings.py`, - tasks defined in each Django app are found and registered, - code running inside Django (views, signals) can queue tasks. ## The celery.py module For a project laid out as `shop/manage.py` and `shop/shop/settings.py`, the recommended place is `shop/shop/celery.py`: ```python import os from celery import Celery os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings") app = Celery("shop") app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks() ``` Line by line: 1. **`DJANGO_SETTINGS_MODULE`** — the `celery` command is not `manage.py`, so nothing else tells it which settings to load. It must be set **before** `Celery(...)` is created: Celery installs its Django integration (the "fixup") at app creation only when this environment variable is present. `setdefault` keeps an explicit value from the environment. 2. **`Celery("shop")`** — the single Celery application for the project. Task names still default to the module path of each function, such as `orders.tasks.send_order_confirmation`. 3. **`config_from_object("django.conf:settings", namespace="CELERY")`** — use Django's settings as Celery's configuration. The string form means the worker does not need to serialise the settings object to child processes. The namespace means Celery reads **uppercase names prefixed with `CELERY_`**: `broker_url` becomes `CELERY_BROKER_URL`. 4. **`autodiscover_tasks()`** — with no arguments, Celery asks its Django integration for every installed app's config and imports `<app>.tasks` from each. Discovery is lazy by default and happens when the worker imports its modules. ## The project package's __init__.py ```python from .celery import app as celery_app __all__ = ("celery_app",) ``` This import makes the Celery app exist as soon as Django imports the project package. Without it, a web process might import a module using `@shared_task` before any app exists, and the task would not be bound to your configured app. ## Where tasks and settings go | Piece | Location | |---|---| | Celery app | `shop/celery.py` | | App import at startup | `shop/__init__.py` | | Celery options | `settings.py`, as `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND`, `CELERY_TASK_TIME_LIMIT`, ... | | Tasks | each app's `tasks.py`, decorated with `@shared_task` | | Worker | separate process: `celery -A shop worker -l INFO` | ## Order-confirmation example ```python # orders/tasks.py from celery import shared_task from django.core.mail import send_mail from .models import Order @shared_task def send_order_confirmation(order_id): order = Order.objects.select_related("customer").get(pk=order_id) send_mail( f"Order {order.pk} confirmed", "Thanks for your order.", None, [order.customer.email], ) ``` The view queues it with `send_order_confirmation.delay(order.pk)`. ## Common wiring mistakes | Mistake | Symptom | |---|---| | `DJANGO_SETTINGS_MODULE` set after `Celery(...)` | Worker cannot import models; no `delay_on_commit()` | | Celery options without the `CELERY_` prefix | Options silently ignored; defaults used | | Task defined outside the app's `tasks.py` | Worker reports an unregistered task | | No import in the project package's `__init__.py` | Web process queues through an unconfigured default app | | Worker started with other settings than the web process | Tasks read another database or broker | The last one is easy to miss in deployment: the web server and the worker are two processes with two environments, so the settings module and secrets must be set for both. ## What the Django integration does for you Once `DJANGO_SETTINGS_MODULE` is set, Celery's Django fixup calls `django.setup()` before task modules are imported in the worker, closes (or periodically recycles) database connections around each task so stale connections are not reused, and, unless you configured your own task base class, makes tasks use a Django-aware class that adds `delay_on_commit()`. These are the reasons the order of lines in `celery.py` matters.

  • Why must os.environ.setdefault('DJANGO_SETTINGS_MODULE', ...) come before Celery('shop')?
    Celery decides whether to install its Django fixup when the app is created, by checking that environment variable. If it is missing at that moment, the worker never calls `django.setup()` for you and has no Django-aware task class, so importing task modules that touch models fails with settings or app-registry errors.
  • What breaks if you forget the import in the project package's __init__.py?
    The Celery app is only created when something imports `shop.celery`. The worker usually does, because `-A shop` looks for the app there, but the web process may not, so `@shared_task` functions imported by views resolve to Celery's default app with default configuration instead of yours.

saying these in an interview costs you the question

  • Thinks Celery tasks run inside the Django web process
  • Sets DJANGO_SETTINGS_MODULE after creating the Celery app
  • Keeps a separate celery config file duplicating Django settings without reason
  • Forgets to import the Celery app in the project package's __init__.py
  • Believes a separate django-celery package is still required
open as a page

In Django, when is send_mail() enough, and when do you build an EmailMessage or EmailMultiAlternatives instead?

level: juniorimportance: must knowfreq 62%

basics

~20 s

send_mail() sends one message, optionally with an html_message alternative, to a recipient list that all appear in To. For CC, BCC, Reply-To, custom headers or attachments, build an EmailMessage, or an EmailMultiAlternatives to add body versions.

open as a page

In Django, how would you create a Profile row automatically whenever a new user signs up, using a post_save receiver?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Connect a receiver to post_save with sender=settings.AUTH_USER_MODEL and create the Profile only when the created argument is True, so later saves of the same user do not insert a second profile.

open as a page

A Django post_save receiver keeps a search index in sync, yet some edits never reach it; which ORM calls bypass model signals?

level: middleimportance: must knowfreq 64%

basics

~10 s

QuerySet.update(), bulk_create() and bulk_update() write SQL directly and send no pre_save or post_save; raw SQL sends nothing; DB_CASCADE deletes (Django 6.1) send no delete signals. QuerySet.delete() still sends pre_delete and post_delete per object.

open as a page

A Django view creates an order in a transaction and queues a Celery email task that intermittently raises Order.DoesNotExist; what is wrong?

level: seniorimportance: must knowfreq 60%

basics

~20 s

The task is queued before the transaction commits, so the worker's separate connection may not see the order yet. Queue it with transaction.on_commit(), or Celery's delay_on_commit() on Django projects, so the message is sent only after commit.

open as a page

In Django 6.0 or later, how do you define a thumbnail job with the @task decorator and enqueue it from a view?

level: juniorimportance: should knowfreq 34%

basics

~20 s

Decorate a module-level function with @task from django.tasks, then call make_thumbnail.enqueue(photo.pk) in the view; enqueue() hands the call to the configured TASKS backend and returns a TaskResult instead of running the function in the view.

open as a page

A Django project's Celery worker ignores BROKER_URL in settings.py; how does namespace='CELERY' in config_from_object explain it?

level: middleimportance: should knowfreq 35%

basics

~10 s

With namespace='CELERY', Celery reads each option as an uppercase CELERY_-prefixed Django setting: broker_url becomes CELERY_BROKER_URL. A plain BROKER_URL or a lowercase broker_url in settings.py is never read, so the default broker is used.

open as a page

In a Django project using Celery, why do apps define tasks with @shared_task instead of @app.task, and how are they discovered?

level: middleimportance: should knowfreq 45%

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.

open as a page

Which email backends does Django ship, and which would you configure for local development, automated tests and production?

level: middleimportance: should knowfreq 48%

basics

~20 s

Django ships smtp, console, filebased, locmem and dummy backends. Use console or filebased in development, let the test runner swap in locmem for tests, and use SMTP or a provider's backend in production. Only SMTP is meant for production.

open as a page

In Django, what does mail_admins() send, and which settings decide its recipients, its From address and its subject line?

level: middleimportance: should knowfreq 30%

basics

~10 s

mail_admins() emails the addresses in ADMINS, from SERVER_EMAIL (default 'root@localhost'), with EMAIL_SUBJECT_PREFIX ('[Django] ') prepended to the subject. It returns without sending if ADMINS is empty. mail_managers() does the same for MANAGERS.

open as a page

In Django, why does a post_save receiver not see the ManyToMany values a ModelForm just saved, and how does m2m_changed help?

level: middleimportance: should knowfreq 38%

basics

~10 s

ModelForm.save() saves the instance, firing post_save, and only then writes the many-to-many rows, so the receiver sees the old relations. m2m_changed fires on add, remove, clear and set, with the through model as sender.

open as a page

In Django, where should signal receivers be connected, and why do AppConfig.ready() and dispatch_uid matter for that registration?

level: middleimportance: should knowfreq 52%

basics

~10 s

Define receivers in a signals submodule and import it from AppConfig.ready(), so they connect once after the app registry is loaded; pass dispatch_uid when the same receiver could be connected twice under different identities.

open as a page

In Django 6.x, what actually runs a task enqueued through django.tasks, and how do ImmediateBackend and DummyBackend differ?

level: middleimportance: should knowfreq 36%

basics

~20 s

Django ships no worker. The default ImmediateBackend runs each task synchronously inside enqueue(); DummyBackend stores results and never runs them. Both are for development and tests; production needs a third-party backend with a durable queue and worker process.

open as a page

In Django's Tasks framework, how do you check a task's outcome later from another request using TaskResult and get_result()?

level: middleimportance: should knowfreq 24%

basics

~10 s

Store TaskResult.id, then call task.get_result(id) or the backend's get_result(id) and read status: READY, RUNNING, FAILED or SUCCESSFUL. return_value works only after success; errors holds failures. Built-in backends cannot fetch results across processes.

open as a page

Why should a Django view pass an order's primary key to a Celery task rather than the Order instance itself?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Celery serialises task arguments, JSON by default, and a Django model instance is not JSON-serialisable. Even with pickle, an instance is a stale snapshot; passing order.pk lets the task load current data and handle a deleted row.

open as a page

A Django signup view calls send_mail() and sometimes hangs for minutes when the SMTP relay is slow — why, and how would you fix it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Django's email backends are synchronous, so send_mail() holds the request while it connects and talks SMTP, and the SMTP backend has no timeout unless you set one. Set the mailer's timeout, then move sending off the request into a task run by a real worker.

open as a page

In Django, a post_save receiver calls an external API and raises; what happens to the save and the request, and how do send() and send_robust() differ?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The exception propagates out of save() into the view. Inside an atomic block the row is rolled back; in autocommit the row is already written. send() lets errors escape; send_robust() catches and returns them, but only for signals you send yourself.

open as a page

Why does Django's Tasks framework reject a model instance passed to enqueue(), and what should a thumbnail task receive instead?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Task arguments and return values go through a JSON round-trip so another process can run the task, so a model instance or datetime raises TypeError. Pass the primary key and re-fetch the row inside the task, handling a deleted or uncommitted row.

open as a page

As a Django team lead, when would you allow model signals and when would you require explicit service-function calls instead?

level: principalimportance: should knowfreq 40%

basics

~20 s

Use signals when you cannot edit the sender, such as a third-party app or contrib model, or when one app must react to another without importing it. Inside your own code, prefer explicit service calls: visible, testable, and not bypassed by bulk writes.

open as a page

In Django 6.1, how would you configure separate mailers for transactional and marketing email, and choose one when sending?

level: middleimportance: nice to knowfreq 22%

basics

~10 s

Define both aliases in the MAILERS setting, each with a BACKEND and OPTIONS. Pass using='marketing' to send_mail(), EmailMessage.send() or mail_admins(), or take a backend from django.core.mail.mailers['marketing']. Without using=, the 'default' alias sends.

open as a page

In Django's Tasks framework, how do priority, queue_name, run_after and takes_context change a task, and when is it rejected?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

priority, queue_name and takes_context are set in @task; run_after only through Task.using(), which returns a modified copy. The backend validates each: unsupported deferral or priority, unknown queues or a bad context signature raise InvalidTask.

open as a page