Skip to content

Signals

Django includes a "signal dispatcher" that acts like a notification system. It allows decoupled applications to get notified when actions occur elsewhere in the framework. This is useful when you want to trigger an event based on a model action, like sending a welcome email when a new user is created, without modifying the model code itself.

When to Use Signals

You should use signals only when necessary. If you can handle the logic directly in a model method like save(), that is usually the better choice because it makes the code easier to follow. Signals can make debugging difficult because they hide the flow of logic.

Commonly Used Signals

Django provides a wide range of signals for different parts of the framework. The table below outlines the most frequently used signals of Django.

SignalDescription
pre_saveSent immediately before a model's save() method is called.
post_saveSent immediately after a model's save() method is called.
pre_deleteSent immediately before a model's delete() method is called.
post_deleteSent immediately after a model's delete() method is called.
m2m_changedSent when a ManyToManyField on a model instance is changed.
request_startedSent when Django begins processing an HTTP request.
request_finishedSent when Django finishes delivering an HTTP response to the client.
post_migrateSent at the end of the migrate command. This is useful for initializing data after an application installs.

For a comprehensive list of all built-in signals and their arguments, please refer to the Django Signals Reference.

Creating a Receiver

To receive a signal, you create a function called a receiver. This function must accept a sender argument and a wildcard keyword argument (**kwargs) to handle any additional data sent by the signal.

You can register a receiver using the Signal.connect() method, but it is more common to use the @receiver decorator (see example below).

Connecting to Specific Senders

Some signals are sent very frequently. For example, pre_save is sent whenever any model in your project is saved. If you do not filter this, your receiver function will run for every single model save, which can cause performance issues.

You can restrict a receiver to a specific sender using the sender argument. In the example below, sender=MyModel ensures the handler only runs when a MyModel instance is saved.

python
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import MyModel

@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, created, **kwargs):
    if created:
        print(f"A new instance of {instance} was created!")
    else:
        print(f"The instance {instance} was updated.")

In this example, the my_model_post_save function runs every time MyModel is saved. The created argument is a boolean that tells you if a new record was inserted or updated in the database.

Where to Put Signal Code

You can define signals anywhere, but the standard practice is to create a signals.py file in your application directory. To ensure the signals are registered when Django starts, you must import this module in the ready() method of your apps.py configuration class.

python
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = 'myapp'

    def ready(self):
        import myapp.signals

Preventing Duplicate Signals

By default, every time your signal registration code runs, Django creates a new connection between the signal and the receiver.

In a complex project, the file containing your signals might be imported in multiple places. If it is imported twice, the registration code runs twice. Consequently, your receiver function will execute twice for a single event.

To prevent this, you can provide a unique string identifier as the dispatch_uid argument when connecting the signal. If Django attempts to register a signal with a dispatch_uid that already exists, it will simply ignore the new request, ensuring the receiver is connected only once.

You can use this with the connect() method or the @receiver decorator:

python
from django.core.signals import request_finished
from django.dispatch import receiver

# Using the decorator (Recommended)
# The string can be anything, but it must be unique to this specific signal-receiver pair.
@receiver(request_finished, dispatch_uid="unique_identifier_for_my_callback")
def my_callback(sender, **kwargs):
    print("Request finished!")

# OR using the manual connect method
request_finished.connect(my_callback, dispatch_uid="unique_identifier_for_my_callback")

Custom Signals

You are not limited to the built-in signals provided by Django. You can define your own signals to notify other parts of your application when specific events occur. This allows you to keep your application components decoupled.

To define a custom signal, create an instance of django.dispatch.Signal.

python
import django.dispatch

# Define the signal
pizza_done = django.dispatch.Signal()

To trigger the signal, call the send() method. You must provide the sender argument, and you can add as many keyword arguments as needed.

python
class PizzaStore:
    ...
    def send_pizza(self, toppings, size):
        # Trigger the signal
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
        ...

Disconnecting Signals

If you need to stop a receiver from listening to a signal, you can use the disconnect() method. This is useful if you need to temporarily disable a signal handler or if you are managing complex object lifecycle.

To disconnect effectively, you must pass the exact same arguments (receiver, sender, and dispatch_uid) that were used when the signal was connected.

python
# Disconnect the signal
pizza_done.disconnect(my_receiver, sender=PizzaStore)

Asynchronous Signals

Django supports calling signal receivers both synchronously and asynchronously. This is useful if your application uses async views or tasks and you want to avoid blocking the main thread during event notifications.

Sending Asynchronous Signals

To trigger a signal in an asynchronous context, use the asend() method. Unlike the standard send() method, asend() is a coroutine and must be awaited.

python
import django.dispatch

pizza_done = django.dispatch.Signal()

async def trigger_signal():
    # You must await asend() in an async environment
    await pizza_done.asend(sender=PizzaStore, toppings=["pepperoni"])

Async Receivers

You can define your receiver functions as asynchronous using async def. Django will automatically detect if a receiver is a coroutine and handle it appropriately.

  • Synchronous Signals (send): If you trigger a signal using send(), Django will run all receivers synchronously. If an async receiver is connected, Django will run it in a thread-safe manner within an event loop.
  • Asynchronous Signals (asend): If you trigger a signal using asend(), Django will await all asynchronous receivers concurrently. Any synchronous receivers will be executed in a thread to avoid blocking the event loop.
python
from django.dispatch import receiver

@receiver(pizza_done)
async def my_async_handler(sender, **kwargs):
    # Perform async tasks like calling an external API
    await asyncio.sleep(1)
    print("Async signal processed")

NOTE

While asend() allows for concurrent execution of receivers, the signal dispatcher itself is still a linear notification system. It does not replace a dedicated task queue like Celery, which is better suited for heavy background processing.