Introduction to Django Signals
This article introduces Django signals and explains how they allow different parts of an application to respond to events such as model saves, deletions, many-to-many changes, requests, and user authentication. It covers receivers, senders, signal registration, common built-in signals, custom signals, transactions, testing, common mistakes, and when explicit function calls are a better choice.
Introduction to Django Signals
Django applications are made of components that perform different jobs.
A model saves data. A view processes a request. A user logs in. A many-to-many relationship changes. A database transaction completes.
Sometimes another part of the application needs to react when one of these events occurs.
For example:
- create a profile when a user account is created
- clear cached data after a model changes
- record an audit entry after an object is deleted
- send a notification after an order is placed
- update related data when a relationship changes
Django signals provide a way to respond to these events without placing all the response logic directly inside the code that caused them.
Django describes signals as a notification system in which a sender informs one or more receivers that an action has occurred. They are most useful when multiple parts of an application may need to respond to the same event.
What Is a Signal?
A signal represents an event.
When the event occurs, the signal is sent. Functions connected to that signal are then called.
The main parts are:
| Part | Purpose |
|---|---|
| Signal | Represents an event |
| Sender | The object or class that sends the signal |
| Receiver | A function that runs when the signal is sent |
| Connection | Registers a receiver with a signal |
The basic flow is:
⧉
1 2 3 4 5 6 7 | |
For example, Django sends the post_save signal after a model instance is saved.
A receiver can listen for that signal:
⧉
1 2 3 4 5 6 7 8 9 | |
Whenever an Article is saved, Django calls article_saved().
Why Signals Exist
Without signals, one component must call every action that should happen afterward.
For example:
⧉
1 2 3 4 | |
This code is explicit and easy to follow, but the component saving the article must know about every related action.
Signals allow those actions to register separately:
⧉
1 | |
The save operation emits a signal, and connected receivers respond.
This can be useful when:
- an event has several independent listeners
- reusable applications need to react to framework events
- the sender should not depend directly on every receiver
- behavior belongs outside the main operation
However, signals can also make program flow harder to trace because an ordinary operation such as save() may trigger code in another module. Django’s documentation warns that signals can create code that is difficult to understand, modify, and debug. When the sender and receiver are both controlled by the same project, an explicit function call is often clearer.
A Simple Model Signal
Suppose an application has an Article model:
⧉
1 2 3 4 5 6 7 8 9 | |
A receiver can react whenever an article is saved:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
The receiver receives information about the event through its arguments.
Receiver Arguments
A signal receiver usually accepts:
⧉
1 2 | |
The available keyword arguments depend on the signal.
For post_save, common arguments include:
| Argument | Purpose |
|---|---|
sender |
Model class that sent the signal |
instance |
Model instance that was saved |
created |
True if a new record was created |
raw |
Whether the model was saved in raw mode |
using |
Database alias used |
update_fields |
Fields passed through update_fields |
kwargs |
Additional signal arguments |
A typical receiver looks like this:
⧉
1 2 3 4 5 6 7 8 9 10 11 | |
Receivers commonly include **kwargs so they remain compatible if the signal provides additional arguments.
Connecting a Receiver
There are two common ways to connect a receiver to a signal:
- using the
@receiverdecorator - calling the signal’s
connect()method
Using the @receiver Decorator
The decorator approach is concise:
⧉
1 2 3 4 5 6 7 | |
The decorator connects article_saved() to post_save.
The sender=Article argument limits the receiver to saves involving the Article model.
Without a sender:
⧉
1 2 3 | |
the receiver runs whenever any model sends post_save.
That is rarely desirable unless the receiver intentionally handles several model types.
Using connect()
A receiver can also be connected directly:
⧉
1 2 3 4 5 6 7 8 9 10 11 | |
Both approaches register the same kind of connection.
The decorator is often easier to read when the receiver is defined in the same module.
Direct connection can be useful when registration must be performed dynamically or when the receiver should remain independent from the decorator.
Where to Put Signal Receivers
A common project structure places receivers in a signals.py file:
⧉
1 2 3 4 5 6 7 8 | |
Example signals.py:
⧉
1 2 3 4 5 6 7 8 9 10 | |
Creating the file is not enough. Python must import it before the receiver can be registered.
A common place to perform that import is the app configuration.
Registering Signals in AppConfig.ready()
Open the app’s apps.py file:
⧉
1 2 3 4 5 6 7 8 9 | |
Importing signals causes the receiver decorators or connect() calls in that module to run.
The import is placed inside ready() to avoid importing application models before Django’s app registry is ready.
A more explicit import that avoids an unused-import warning is:
⧉
1 2 3 4 5 6 7 8 9 | |
The app configuration must be loaded through INSTALLED_APPS.
Modern Django applications commonly use the app name:
⧉
1 2 3 4 | |
Django can normally discover the app’s default configuration.
It may also be specified explicitly:
⧉
1 2 3 4 | |
Common Model Signals
Django provides several signals for model activity. The most commonly used are:
| Signal | Sent when |
|---|---|
pre_init |
A model instance begins initialization |
post_init |
A model instance finishes initialization |
pre_save |
Before a model instance is saved |
post_save |
After a model instance is saved |
pre_delete |
Before a model instance is deleted |
post_delete |
After a model instance is deleted |
m2m_changed |
A many-to-many relationship changes |
class_prepared |
A model class is prepared |
The exact arguments vary by signal. Django maintains a reference containing the arguments provided by each built-in signal.
pre_save
pre_save is sent before a model instance is saved.
⧉
1 2 3 4 5 6 7 8 9 | |
This example removes surrounding whitespace before the article is saved.
Although this works, simple model-specific normalization may be clearer in:
- a form
- a model method
- a service function
- an overridden
save()method
A signal is most useful when the behavior should remain separate from the model’s main implementation.
post_save
post_save is sent after a model instance is saved.
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
The created argument distinguishes between insertion and update:
⧉
1 2 3 4 5 | |
Creating Related Objects with post_save
A common introductory example creates a profile when a user account is created.
Profile model:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Receiver:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
There is an important problem with this example: sender expects a model class, while settings.AUTH_USER_MODEL is a string such as "accounts.User".
Django signals support lazy sender references for model signals, so a string reference may be used:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
Another approach is to use the configured model class:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Be careful with this pattern. If profile creation is required for the account-creation workflow, calling a dedicated service function may make the dependency clearer and easier to test.
pre_delete
pre_delete is sent before an object is deleted.
⧉
1 2 3 4 5 6 7 8 9 | |
At this point, the object still exists in the database.
This can be useful when information must be collected before deletion.
post_delete
post_delete is sent after an object is deleted.
⧉
1 2 3 4 5 6 7 8 9 | |
The Python instance still exists in memory, but its database row has been removed.
A common use is deleting files associated with a model:
⧉
1 2 3 4 | |
This should be implemented carefully. Shared files, storage errors, transactions, and rollback behavior can make automatic file deletion more complicated than the example suggests.
m2m_changed
m2m_changed is sent when a ManyToManyField relationship changes.
Models:
⧉
1 2 3 4 5 6 7 8 9 10 | |
Receiver:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
The sender is the intermediate model:
⧉
1 | |
Common action values include:
| Action | Meaning |
|---|---|
pre_add |
Before relationships are added |
post_add |
After relationships are added |
pre_remove |
Before relationships are removed |
post_remove |
After relationships are removed |
pre_clear |
Before all relationships are cleared |
post_clear |
After all relationships are cleared |
Example:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Request and Response Signals
Django also provides signals related to request processing.
Common examples include:
| Signal | Sent when |
|---|---|
request_started |
Django begins processing a request |
request_finished |
Django finishes a response |
got_request_exception |
An exception occurs during request handling |
Example:
⧉
1 2 3 4 5 6 7 | |
Request signals should be used sparingly. Middleware usually provides a clearer way to implement logic that should run around every request. Django’s signal reference specifically recommends considering middleware before request and response signals because signals can make request flow harder to maintain.
Authentication Signals
Django’s authentication system sends signals for login-related events.
Common authentication signals include:
⧉
1 2 3 4 5 | |
user_logged_in
Sent after a user logs in:
⧉
1 2 3 4 5 6 7 | |
user_logged_out
Sent when a user logs out:
⧉
1 2 3 4 5 6 7 | |
user_login_failed
Sent when authentication fails:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
Avoid recording submitted passwords or other sensitive credentials. Authentication-related logging must be designed carefully to prevent sensitive information from appearing in logs.
The sender Argument
The sender identifies the source of the event.
For a model signal:
⧉
1 2 3 | |
the sender is the Article class.
This lets a receiver listen only for events from one model.
Without sender:
⧉
1 2 3 | |
the receiver listens for post_save from every model.
You can inspect the sender:
⧉
1 2 3 | |
Specifying a sender usually makes receivers more focused and avoids unnecessary calls.
Preventing Duplicate Registration
A receiver can sometimes be connected more than once.
This may happen because:
- a signals module is imported repeatedly
- app initialization runs more than expected in tests
- a receiver is registered dynamically
- development auto-reloading imports code again
A duplicate receiver may cause an action to run multiple times.
Use dispatch_uid to identify a connection:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
The unique identifier helps Django avoid registering the same logical receiver more than once.
The decorator also supports dispatch_uid:
⧉
1 2 3 4 5 6 7 | |
Weak References
Django stores signal receivers as weak references by default.
This means a locally defined receiver may be garbage-collected if nothing else keeps a strong reference to it.
A strong connection can be requested:
⧉
1 2 3 4 5 | |
Module-level receiver functions normally remain available, so beginners rarely need to change weak.
It matters more when receivers are local functions, dynamically created functions, or callable objects with limited lifetimes.
Disconnecting a Receiver
Use disconnect() to remove a receiver:
⧉
1 2 3 4 | |
When dispatch_uid was used:
⧉
1 2 3 4 | |
Disconnecting signals can be useful in:
- tests
- temporary maintenance code
- dynamic plugin systems
- situations where a receiver must be disabled temporarily
In ordinary application code, connections usually remain active for the lifetime of the process.
Custom Signals
Django allows applications to define their own signals.
⧉
1 2 3 4 | |
Send the signal:
⧉
1 2 3 4 | |
Connect a receiver:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
The sender can be any Python object or class representing the source of the event.
send() and send_robust()
Custom signals can be sent with:
⧉
1 | |
or:
⧉
1 | |
With send(), an exception raised by a receiver interrupts signal dispatch and propagates to the caller.
With send_robust(), receiver exceptions are collected in the returned results rather than immediately stopping dispatch.
Example:
⧉
1 2 3 4 | |
The result contains pairs of receivers and their returned values or exceptions.
Use this deliberately. Silently collecting exceptions may hide failures unless the results are inspected and logged.
When Custom Signals Are Appropriate
A custom signal may be useful when:
- a reusable app exposes an event to outside applications
- several independent components need to react
- the sender should not import receivers
- third-party code should be able to subscribe
- listeners are optional extensions
For example, a reusable payment package may emit:
⧉
1 | |
without knowing whether the host project will:
- send an email
- create an invoice
- update analytics
- award loyalty points
When an Explicit Function Call Is Better
Suppose an order must reserve inventory before it is considered complete.
A signal might hide that requirement:
⧉
1 | |
Somewhere else:
⧉
1 2 3 | |
A developer reading the save operation cannot see that inventory reservation is a required part of the workflow.
An explicit service function is clearer:
⧉
1 2 3 4 5 6 7 8 | |
This makes the sequence and dependencies visible.
Use explicit calls when:
- the action is required
- execution order matters
- failure must stop the operation
- the sender and receiver are in the same project
- the behavior is part of the core business process
- developers must easily trace the workflow
Use signals when the reactions are optional, independent, or genuinely decoupled.
Signals and Database Transactions
A post_save receiver runs after the model’s save() method completes, but that does not always mean the surrounding database transaction has been committed.
For example:
⧉
1 2 3 4 5 | |
A post_save receiver runs inside the transaction.
If the transaction later rolls back, external work already performed by the receiver may not be rolled back.
This matters for actions such as:
- sending email
- publishing messages to a queue
- calling external APIs
- clearing shared caches
- processing files
Use transaction.on_commit() when an action should happen only after a successful commit:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Passing the primary key instead of the model instance can be safer for delayed work because the callback can retrieve the committed database state.
Avoiding Recursive Signals
A receiver may accidentally trigger itself.
Example:
⧉
1 2 3 4 | |
The second save() sends post_save again, causing recursion.
Possible solutions include checking whether the update is needed:
⧉
1 2 3 4 5 6 7 8 | |
A queryset update() does not call the model’s save() method and does not send pre_save or post_save.
This prevents recursion, but it also means other save-related behavior will not run.
An explicit service function may be clearer if the update is part of the normal workflow.
Signals and QuerySet Operations
Not every database operation sends model signals in the same way.
For example:
⧉
1 2 3 | |
uses a direct SQL update.
It does not call each object’s save() method, so pre_save and post_save are not sent for each updated object.
Similarly, bulk operations may bypass normal per-instance behavior.
Do not build critical correctness rules around signals unless you understand every path that can modify the data.
Database constraints, model validation, service functions, and explicit workflows may provide stronger guarantees.
Keep Receivers Small
Signal receivers should usually remain short.
Avoid placing a large business workflow directly in the receiver:
⧉
1 2 3 4 5 | |
Prefer delegating to a clearly named function:
⧉
1 2 3 4 | |
This makes the receiver easy to inspect and the main logic easier to test directly.
Avoid Unnecessary Database Queries
Signals can run frequently.
A receiver connected to post_save may run every time an object is updated.
Avoid unnecessary queries:
⧉
1 2 3 | |
The signal already provides the saved instance:
⧉
1 2 3 | |
Query only when the receiver needs refreshed or related data that is not already available.
Testing Signal Receivers
Receivers can be tested by triggering the event:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
The receiver’s delegated function can also be tested directly:
⧉
1 2 | |
For focused unit tests, mocking can verify that the receiver delegates correctly:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Tests should confirm both:
- the receiver is connected
- the intended side effect occurs
Temporarily Disabling Signals in Tests
Signals can make tests harder to isolate.
A receiver can be disconnected temporarily:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Always reconnect the receiver, preferably with a fixture or context manager that guarantees cleanup.
Frequently disabling signals in tests may indicate that the application relies too heavily on hidden side effects.
Common Beginner Mistakes
Creating signals.py but Never Importing It
A receiver is not registered until its module is imported.
Use AppConfig.ready():
⧉
1 2 | |
Forgetting **kwargs
Avoid:
⧉
1 2 3 | |
Prefer:
⧉
1 2 3 | |
The signal supplies additional keyword arguments.
Omitting the Sender
This receiver runs for every saved model:
⧉
1 2 3 | |
Use a sender when the receiver is model-specific:
⧉
1 2 3 | |
Performing Required Business Logic in a Hidden Receiver
If an order cannot be completed without reserving inventory, call the inventory logic explicitly.
Do not make essential workflow steps invisible.
Triggering Recursive Saves
This may call itself indefinitely:
⧉
1 2 3 | |
Add a guard, use an appropriate queryset update, or redesign the workflow.
Assuming post_save Means Transaction Committed
A receiver may run before the surrounding transaction successfully commits.
Use transaction.on_commit() for external side effects that must happen only after commit.
Sending Expensive Work Synchronously
A receiver runs as part of the current execution flow.
Slow network calls or heavy processing can delay the request or command that triggered the signal.
Delegate expensive work to an appropriate task system when required.
Hiding Errors
A failed receiver can cause the original operation to fail when the signal uses normal send() behavior.
Handle expected errors deliberately, but do not silently ignore failures that indicate corrupted or incomplete behavior.
Using Signals Everywhere
Signals are not a replacement for ordinary function calls, service objects, model methods, or middleware.
Use them when decoupling provides a real benefit.
A Complete Example
Suppose a blog should record an event when a new article is created.
Models:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Signals:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
App configuration:
⧉
1 2 3 4 5 6 7 8 9 | |
When this runs:
⧉
1 2 3 4 | |
the receiver creates an Activity record.
The caller does not need to call the receiver directly.
Recommended Signal Structure
A simple app can use this organization:
⧉
1 2 3 4 5 6 7 8 | |
signals.py should contain small receivers:
⧉
1 2 3 4 5 6 7 8 9 | |
services.py should contain the main behavior:
⧉
1 2 3 4 5 6 | |
This keeps signal registration separate from the reusable application logic.
When to Use Signals
Signals are a reasonable choice when:
- several independent receivers react to one event
- a reusable app needs extension points
- optional components respond to framework events
- the sender should not import receiver modules
- behavior is secondary to the main operation
- built-in Django events provide the needed hook
Examples include:
- audit logging
- cache invalidation
- analytics events
- optional notifications
- cleanup of related resources
- integration hooks in reusable packages
When Not to Use Signals
Prefer explicit code when:
- the action is required for correctness
- the execution order matters
- the caller needs the result
- failures must be handled directly
- the behavior is part of one business workflow
- the sender and receiver are tightly related
- a model method or service function is clearer
- middleware better matches request-wide behavior
A useful rule is:
Use signals for notifications about an event, not to hide the main process that performs the event.
Mini Reference
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Common model signals:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Common authentication signals:
⧉
1 2 3 | |
Basic receiver:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 | |
Signal registration:
⧉
1 2 3 4 5 | |
Transaction-safe external action:
⧉
1 2 3 | |
Django signals allow one part of an application to respond when an event occurs elsewhere.
The central ideas are:
- a signal represents an event
- a sender emits the signal
- receivers listen for the signal
- receivers can be connected with
@receiverorconnect() - Django provides model, request, authentication, and other built-in signals
- receivers are commonly placed in
signals.py - signal modules must be imported during app initialization
- receivers should remain small and focused
- required business workflows are usually clearer as explicit function calls
- database transactions must be considered before performing external side effects
- signals should reduce unwanted dependencies without hiding important application behavior
Signals are useful, but they should be introduced carefully. Start with explicit code. Use signals when the event genuinely needs independent listeners or when Django already provides a natural signal for the behavior you need.
Join the Newsletter
Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.
Get updates when new guides, learning paths, cheat sheets, and field notes are published.
No spam. Unsubscribe anytime.
There is no third-party involved so don't worry - we won't share your details with anyone.