~5 min read • Updated Mar 15, 2026
What Are Django Signals?
Django includes a built‑in signal dispatcher that allows different parts of an application to communicate without tight coupling. Signals let certain senders notify a set of receivers when an action occurs. They are especially useful when multiple components need to react to the same event.
For example, a third‑party app can listen for settings changes:
from django.apps import AppConfig
from django.core.signals import setting_changed
def my_callback(sender, **kwargs):
print("Setting changed!")
class MyAppConfig(AppConfig):
def ready(self):
setting_changed.connect(my_callback)
Warning: Signals may appear to promote loose coupling, but overuse can lead to code that is difficult to understand and debug. Whenever possible, call functions directly instead of relying on signals.
Listening to Signals
To receive a signal, you must register a receiver function using Signal.connect().
Each receiver is called in the order it was registered.
connect() Parameters
- receiver — the callback function
- sender — restricts signals to a specific sender
- weak — prevents garbage collection when set to False
- dispatch_uid — prevents duplicate registrations
Defining Receiver Functions
def my_callback(sender, **kwargs):
print("Request finished!")
All receivers must accept sender and **kwargs. Even if a signal currently sends no arguments, Django may add them later.
Asynchronous Receivers
async def my_callback(sender, **kwargs):
await asyncio.sleep(5)
print("Request finished!")
Django automatically adapts receivers to synchronous or asynchronous signal dispatch.
Connecting Receivers
Method 1: Manual Connection
from django.core.signals import request_finished
request_finished.connect(my_callback)
Method 2: Using the @receiver Decorator
from django.core.signals import request_finished
from django.dispatch import receiver
@receiver(request_finished)
def my_callback(sender, **kwargs):
print("Request finished!")
This approach is cleaner and more maintainable.
Where Should Signal Code Live?
Although signal code can technically live anywhere, best practice is:
- Create a signals.py module inside your app
- Connect signals inside the ready() method of your AppConfig
Example:
from django.apps import AppConfig
class MyAppConfig(AppConfig):
def ready(self):
from . import signals
Importing the signals module ensures that all @receiver decorators are executed.
Connecting to Signals from Specific Senders
Some signals fire frequently, but you may only care about events from a specific model.
For example, pre_save fires before any model is saved, but you can restrict it:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel
@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
...
Now the receiver runs only when MyModel is saved.
Best Practices for Using Signals
- Use signals sparingly—they can make code harder to trace
- Always accept **kwargs in receivers
- Use dispatch_uid to avoid duplicate registrations
- Be aware that ready() may run multiple times during testing
Conclusion
Django signals provide a powerful event‑driven mechanism for building decoupled applications. By defining receivers, using decorators, restricting senders, and organizing signal code properly, you can create flexible and maintainable event‑handling systems. However, signals should be used thoughtfully to avoid unnecessary complexity.
Preventing Duplicate Signal Registrations
By default, Django identifies receivers using their Python object identity. For module‑level functions, static methods, and class methods, this identity is stable—so connecting the same receiver multiple times has no effect:
def my_handler(sender, **kwargs):
...
my_signal.connect(my_handler) # Running again does nothing.
Bound Methods Behave Differently
Bound methods depend on the instance they belong to. Each time you create a new instance, the method has a new identity:
def connect_signals():
backend = Backend()
my_signal.connect(backend.my_handler)
connect_signals() # Registers a new receiver each time.
To prevent duplicate registrations, use dispatch_uid:
from django.core.signals import request_finished
request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")
Defining Custom Signals
You can define your own signals using django.dispatch.Signal.
import django.dispatch
pizza_done = django.dispatch.Signal()
Important: If both sender and receiver are inside your own project, a direct function call is usually better than a signal.
Sending Signals
Django provides four methods for sending signals:
- send() — synchronous, errors propagate
- send_robust() — synchronous, catches exceptions
- asend() — asynchronous, must be awaited
- asend_robust() — asynchronous, catches exceptions
Example: Sending a Custom Signal
class PizzaStore:
def send_pizza(self, toppings, size):
pizza_done.send(
sender=self.__class__,
toppings=toppings,
size=size
)
All send methods return a list of tuples:
[(receiver, response), ...]
send() vs send_robust()
- send() — stops if a receiver raises an exception
- send_robust() — continues calling all receivers and returns the exception object
Exceptions returned by send_robust() include traceback information via __traceback__.
Asynchronous Signal Sending
asend() and asend_robust() are async coroutines:
async def asend_pizza(self, toppings, size):
await pizza_done.asend(
sender=self.__class__,
toppings=toppings,
size=size
)
How Django Handles Sync and Async Receivers
- Async receivers are executed concurrently using
asyncio.gather() - Sync receivers are wrapped with
sync_to_async()when called fromasend() - Async receivers are wrapped with
async_to_sync()when called fromsend()
To reduce overhead, Django groups receivers by sync/async type before calling them. This means execution order may differ slightly from registration order.
Disconnecting Signals
To remove a receiver:
Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)
The method returns:
- True — a receiver was disconnected
- False — no matching receiver found
- None — when sender is a lazy reference
If you used dispatch_uid, you can disconnect without passing the receiver function.
Conclusion
Django’s signal system is powerful and flexible, enabling decoupled communication between components. By using dispatch_uid, you can prevent duplicate registrations. With send(), send_robust(), asend(), and asend_robust(), you can send signals safely in both synchronous and asynchronous contexts. Finally, disconnect() gives you full control over removing receivers when needed.
Written & researched by Dr. Shahin Siami