Django Signals — Core Concepts
What signals solve
Django applications grow into multiple apps that need to react to shared events. When a User model is saved, the auth system handles the record — but your analytics app wants to log it, your notifications app wants to send a welcome message, and your CRM integration wants to sync data.
Without signals, the auth app would need to import and call each of these directly. That creates tight coupling: the auth app depends on analytics, notifications, and CRM. Signals break that dependency by creating an event bus within Django.
Built-in signal types
Django ships with several pre-built signals:
pre_save/post_save— fire before and after a model’ssave()methodpre_delete/post_delete— fire before and after deletionm2m_changed— fires when a ManyToMany relationship is modifiedrequest_started/request_finished— fire at HTTP request boundariespre_migrate/post_migrate— fire during database migration runs
The post_save signal is the most commonly used. It provides the saved instance and a created boolean that tells you whether this is a new record or an update.
Connecting a signal handler
A signal handler is a regular function that receives the sender (usually a model class) and keyword arguments:
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Order
@receiver(post_save, sender=Order)
def on_order_saved(sender, instance, created, **kwargs):
if created:
send_confirmation_email(instance)
The @receiver decorator registers this function to run every time an Order is saved. The **kwargs catch is important — Django may add new arguments to signals in future versions, and your handler needs to accept them.
Where to register signals
Signal handlers must be registered before they can fire. Django’s recommended pattern is to put them in a signals.py file inside your app, then import that file from the app’s ready() method:
# myapp/apps.py
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
import myapp.signals # noqa: F401
Forgetting this step is the most common reason signals “don’t work” — the handler function exists but was never connected.
Execution is synchronous
Signals execute in the same database transaction and thread as the code that triggered them. A post_save handler runs immediately after save() returns, before the view sends its HTTP response.
This means a slow handler slows down the entire request. If your handler sends an email or calls an external API, the user waits for that to complete. For anything that takes more than a few milliseconds, queue the work with Celery or a similar task runner instead of doing it in the signal.
When signals are the wrong tool
Signals shine when decoupling apps that shouldn’t know about each other. But within a single app, they add indirection without benefit. If model A and model B are in the same app, calling a function directly is clearer than routing through a signal.
Common antipatterns:
- Using signals to implement core business logic (makes debugging a maze)
- Chains of signals where one handler triggers another, which triggers another
- Putting heavy computation in signal handlers instead of background tasks
A useful rule: if you can’t explain why the signal is better than a direct function call, use the direct call.
Common misconception
Many developers assume post_save fires after the database transaction commits. It doesn’t — it fires after save() but potentially before the transaction completes. If another process queries the database at that moment, it might not see the saved data yet. Django provides transaction.on_commit() for actions that need confirmed persistence.
The one thing to remember: Signals are Django’s event system for decoupling apps — powerful for cross-app reactions, but direct function calls are usually better within a single app.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.