In Django, where should signal receivers be connected, and why do AppConfig.ready() and dispatch_uid matter for that registration?
answer
- import side effects
- the app registry is ready
- receiver identity decides duplicates
- a stable string key
basics
~10 sDefine 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.
solid answer
~40 sThe docs recommend keeping receivers out of `models.py` and the app's root module, putting them in `signals.py` and importing that module inside `AppConfig.ready()`. `ready()` runs once the app registry is populated, so every model is importable and the receivers are connected before requests or commands use them. Django deduplicates receivers by the identity of the function plus the sender, so connecting the same module-level function twice is a no-op. Bound methods on a recreated instance, or a module imported under two paths, produce distinct identities and register twice. A `dispatch_uid` string replaces that identity as the key, so the receiver is registered once per uid and sender.
code
python · 15 linesfrom django.apps import AppConfig
from django.db.models.signals import post_save
class OrdersConfig(AppConfig):
name = "orders"
def ready(self):
from . import signals
post_save.connect(
signals.reindex_order,
sender="orders.Order",
dispatch_uid="orders.reindex_order",
)go deeper
Remember that @receiver only runs at import time, so the signals module has to be imported from the app's AppConfig.ready().
Explain the lookup key made from receiver and sender identity, how dispatch_uid replaces it, and why weak references can drop nested functions.
Diagnose double-firing receivers in production or tests by tracing duplicate imports and ready() re-runs, and standardise dispatch_uid naming across apps.
Treat receiver registration as part of app boundaries: an app should connect only to its own or explicitly public senders, which keeps cross-app coupling visible.
## How a receiver gets connected A **receiver** is any callable that accepts `**kwargs`. It is connected to a **signal** in one of two ways: - `some_signal.connect(func, sender=..., weak=True, dispatch_uid=None)`, or - the `@receiver(some_signal, sender=...)` decorator from `django.dispatch`, which calls `connect()` when the decorated function is defined — that is, **when its module is imported**. That last point drives everything else. A decorated receiver in a module nobody imports is never connected, and nothing warns you. ## Why AppConfig.ready() Each Django app has an **application configuration** class in `apps.py`. Django populates the **app registry** at startup and then calls each config's `ready()` method once the registry is complete. The docs recommend this pattern: ```python # orders/apps.py from django.apps import AppConfig class OrdersConfig(AppConfig): name = "orders" def ready(self): from . import signals # noqa: F401 ``` Reasons it is the right place: 1. **Models are loaded.** Receivers usually import models; importing them before the registry is ready raises `AppRegistryNotReady`. 2. **It runs for every entry point.** WSGI, ASGI, `manage.py` commands and the test runner all call `django.setup()`, which calls `ready()`. 3. **It avoids import side effects in `models.py`.** The docs advise keeping registration out of the app's root module and its `models` module, to minimise what importing a model drags in and to avoid circular imports. The docs also note that `ready()` may run more than once during testing, which is exactly where duplicate registration shows up. ## How duplicates are detected Inside `Signal.connect()`, Django builds a **lookup key**. Without a `dispatch_uid` the key is `(id of the receiver, id of the sender)`; with one it is `(dispatch_uid, id of the sender)`. If that key is already registered, the new connection is silently skipped. | Receiver kind | Connect it twice | Result | |---|---|---| | Module-level function | Same function object | Registered once | | Bound method, new instance each time | Different object each time | Registered **twice** | | Same function, module imported under two names | Two function objects | Registered **twice** | | Any of the above with `dispatch_uid="orders.index"` | Same uid and sender | Registered once | When a receiver is registered twice, it **runs twice per event**: two welcome emails, two audit rows, a doubled counter. `dispatch_uid` is the documented fix: ```python @receiver(post_save, sender="orders.Order", dispatch_uid="orders.reindex_order") def reindex_order(sender, instance, **kwargs): ... ``` ## Weak references `connect()` defaults to `weak=True`: the signal holds a weak reference to the receiver. A module-level function stays alive because its module holds it. A function defined **inside another function** can be garbage-collected as soon as that outer function returns, and the receiver disappears. Pass `weak=False` for such closures, or define receivers at module level. ## Choosing the sender `sender=None` (the default) receives the signal from **every** sender — a `post_save` receiver without `sender` runs for every model save in the project, including sessions and admin log entries. For model signals you normally pass the model class or its lazy `"app_label.ModelName"` string. `disconnect()` takes the same `receiver`, `sender` and `dispatch_uid` and returns whether something was removed, which is useful in tests. ## Request signals Django connects itself Signals are not only a user feature. `request_started` and `request_finished` are sent by the WSGI and ASGI handlers, and Django's own `django.db` module connects `close_old_connections` to both of them, which is how connections past `CONN_MAX_AGE` are closed between requests. A project can connect its own receivers the same way — but keep them cheap, because they run on every request. ## Checklist - Receivers live in `app/signals.py`, never registered from `models.py`. - `ready()` imports that module. - Model receivers always name a `sender`. - Bound-method or dynamically created receivers carry a `dispatch_uid`. - Nested functions are not connected with the default weak reference.
- If a module-level receiver is deduplicated by identity anyway, why do people still see it fire twice?Usually the module was imported under two names, for example `signals` through a path hack and `orders.signals` normally, which creates two function objects. Bound methods on instances created in `ready()` or a test setup do the same. Each distinct object gets its own lookup key, so a `dispatch_uid` or fixing the import path removes the duplicate.
- What does Django connect to request_finished, and why does that matter for your own receivers?`django.db` connects `close_old_connections` to both `request_started` and `request_finished`, and the cache module connects a function that closes cache connections to `request_finished`. Your receivers on these signals run for every request in the same thread, so anything slow there adds to every request's cost.
saying these in an interview costs you the question
- Registers receivers by importing them at the bottom of models.py
- Assumes a decorated receiver connects itself without its module being imported
- Omits sender on a post_save receiver meant for a single model
- Connects a nested function with the default weak reference and wonders why it stops firing
- Thinks dispatch_uid is a routing key that selects which receiver runs