The best examples of Django signals: 3 practical examples that actually matter

If you’ve used Django for more than a week, you’ve probably heard people mention signals with a mix of respect and suspicion. They’re powerful, easy to abuse, and often misunderstood. In this guide, we’ll walk through the best examples of Django signals: 3 practical examples that show when they genuinely earn their place in your codebase. Along the way, we’ll layer in several more real examples so you can see how they behave in day‑to‑day projects, not just toy snippets. These examples of Django signals focus on problems you actually hit in production: keeping data in sync, sending notifications, and integrating with external systems without turning every view into a ball of side‑effects. You’ll see an example of using `post_save` for profile creation, another for analytics and logging, and a third for asynchronous tasks. After that, we’ll look at variations and patterns you can reuse across projects, plus some 2024‑ready advice on when you should skip signals entirely.
Written by
Jamie
Published

Let’s skip theory and start with concrete code. These examples of Django signals focus on three problems:

  • Keeping related models in sync
  • Tracking behavior for analytics and audit logs
  • Kicking off background work without bloating your views

All three are real examples I’ve seen in production on Django 4.x and 5.x.


Example of Django signal #1: Auto‑create a user profile

This is the classic example of Django signals: 3 practical examples articles almost always start here, and for good reason. You have a User model and a Profile model. Every time a user is created, you want a profile to exist too.

Models:

## accounts/models.py
from django.conf import settings
from django.db import models

class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    timezone = models.CharField(max_length=64, default="UTC")
    marketing_opt_in = models.BooleanField(default=False)

    def __str__(self):
        return f"Profile({self.user.username})"
``

**Signal receiver (using `post_save`):**

```python
## accounts/signals.py
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Profile

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_user_profile(sender, instance, created, **kwargs):
    if not created:
        return

    Profile.objects.get_or_create(user=instance)

App config to ensure import:

## accounts/apps.py
from django.apps import AppConfig

class AccountsConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "accounts"

    def ready(self):
        from . import signals  # noqa: F401

And in settings.py:

INSTALLED_APPS = [
#    # ...
    "accounts.apps.AccountsConfig",
]

This example of a Django signal keeps your data consistent without littering user‑creation logic across views, forms, and admin actions. Whether the user is created via the admin, a REST API, or a management command, the profile just appears.

Variations on this pattern (more real examples):

You can reuse this pattern in several ways:

  • API keys: Auto‑generate API tokens when a ServiceAccount is created.
  • Onboarding tasks: Create a default workspace, team, or project when a new user signs up.
  • Permissions: Attach default groups or roles when a user is created.

All of these are solid examples of Django signals when the behavior really is global and should happen regardless of how the object was created.


Example of Django signal #2: Audit log and analytics with post_save and post_delete

In 2024, analytics and auditing aren’t nice‑to‑have extras. You’re expected to know who changed what, and when. Instead of sprinkling logging calls across views, you can use signals to capture changes in a single place.

This second entry in our examples of Django signals: 3 practical examples shows how to log changes to an Order model.

Models:

## shop/models.py
from django.conf import settings
from django.db import models

class Order(models.Model):
    STATUS_CHOICES = [
        ("pending", "Pending"),
        ("paid", "Paid"),
        ("shipped", "Shipped"),
        ("canceled", "Canceled"),
    ]

    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    total_cents = models.PositiveIntegerField()
    status = models.CharField(max_length=16, choices=STATUS_CHOICES, default="pending")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)


class OrderEvent(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="events")
    event_type = models.CharField(max_length=64)
    metadata = models.JSONField(default=dict, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

Signals for audit events:

## shop/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

from .models import Order, OrderEvent

@receiver(post_save, sender=Order)
def log_order_save(sender, instance, created, **kwargs):
    if created:
        event_type = "order_created"
    else:
        event_type = "order_updated"

    OrderEvent.objects.create(
        order=instance,
        event_type=event_type,
        metadata={
            "status": instance.status,
            "total_cents": instance.total_cents,
        },
    )


@receiver(post_delete, sender=Order)
def log_order_delete(sender, instance, **kwargs):
    OrderEvent.objects.create(
        order=instance,
        event_type="order_deleted",
        metadata={
            "status": instance.status,
            "total_cents": instance.total_cents,
        },
    )

This is one of the best examples of Django signals for analytics because:

  • It centralizes logging logic.
  • It works regardless of how the order is created or modified.
  • It plays nicely with admin actions, custom scripts, and API calls.

You can plug this data into dashboards, send it to a warehouse, or feed it into anomaly detection. While health‑focused organizations might send data into HIPAA‑compliant systems and follow guidance from sources like the U.S. Department of Health & Human Services, the pattern is the same: signals trigger consistent audit trails.

More real examples that use this pattern:

These examples of Django signals include several variants:

  • GDPR/CCPA logging: Track when users update privacy settings or delete accounts.
  • Content moderation: Log when posts are flagged, hidden, or restored.
  • Financial records: Record status changes on invoices or payouts.

In regulated sectors like healthcare or research, audit trails are often discussed in guidance from organizations such as NIH or universities like Harvard. Django signals don’t make you compliant by themselves, but they give you a reliable hook to capture the events you need.


Example of Django signal #3: Kick off background tasks when data changes

Our third entry in the list of examples of Django signals: 3 practical examples moves beyond the database. Modern Django apps rarely live alone; they send emails, push notifications, or analytics events to external services.

You can use signals to trigger background jobs when certain models change, while keeping the heavy work out of the request/response cycle.

Assume you’re using Celery for background tasks.

Task:

## notifications/tasks.py
from celery import shared_task

@shared_task
def send_order_confirmation_email(order_id):
    from shop.models import Order

    order = Order.objects.select_related("user").get(pk=order_id)
#    # Do the actual email sending here

Signal to enqueue the task:

## shop/signals.py (continuing from earlier)
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Order
from notifications.tasks import send_order_confirmation_email

@receiver(post_save, sender=Order)
def trigger_order_confirmation_email(sender, instance, created, **kwargs):
    if not created:
        return

    send_order_confirmation_email.delay(instance.pk)

This is a practical example of Django signals working with asynchronous infrastructure: every time an order is created, an email task is queued. Your view stays thin, and the behavior is enforced system‑wide.

Other background‑task examples include:

  • Indexing: Update a search index (e.g., OpenSearch, Elasticsearch) when content changes.
  • Webhooks: Notify third‑party services when a subscription is renewed or canceled.
  • Data sync: Mirror data to analytics platforms when a model hits certain states.

These are some of the best examples of Django signals when your app needs to react to model changes without blocking the user.


More real examples of Django signals in everyday projects

So far, we’ve covered examples of Django signals: 3 practical examples that revolve around profiles, audit logs, and background tasks. In real projects, you’ll often adapt these patterns into more specialized cases.

Here are additional scenarios where signals fit naturally:

Enforcing business rules across multiple entry points

Imagine a Subscription model that must never overlap for the same user. You might enforce this with database constraints, but you can also use pre_save to normalize or adjust data before it hits the database.

## billing/signals.py
from django.db.models.signals import pre_save
from django.dispatch import receiver

from .models import Subscription

@receiver(pre_save, sender=Subscription)
def normalize_subscription_dates(sender, instance, **kwargs):
    if instance.end_date and instance.start_date > instance.end_date:
        instance.start_date, instance.end_date = instance.end_date, instance.start_date

This is another example of Django signals quietly protecting data integrity no matter where the object is created.

Files, cache entries, and external resources often outlive the models that reference them. A post_delete signal is a clean way to keep your system tidy.

## media/signals.py
import os

from django.conf import settings
from django.db.models.signals import post_delete
from django.dispatch import receiver

from .models import Document

@receiver(post_delete, sender=Document)
def delete_file_on_document_delete(sender, instance, **kwargs):
    if instance.file and os.path.isfile(instance.file.path):
        os.remove(instance.file.path)

You’ll see similar examples of Django signals used to clear cache keys, revoke API tokens, or tear down external resources.


2024–2025 perspective: when to use signals, and when to walk away

Django signals are tempting. In 2024–2025, with more teams using Django for microservices, APIs, and event‑driven systems, it’s easy to overuse them and end up with behavior that’s hard to trace.

Here’s the honest rule of thumb:

  • Use signals for cross‑cutting concerns that should fire no matter how a model is created or changed: audit logs, analytics, cleanup, and some background tasks.
  • Avoid signals for core business workflows where order and visibility matter. In those cases, explicit service functions, domain services, or command handlers are easier to reason about.

Django’s own documentation warns about complexity; the official signals docs at docs.djangoproject.com are worth bookmarking. For more general software‑architecture thinking, you can cross‑reference guidance from academic and industry sources, for example research libraries at universities like MIT when you’re studying event‑driven or domain‑driven design patterns.

When you look back at the examples of Django signals: 3 practical examples in this article, notice the pattern:

  • Each signal is short.
  • Each signal is focused on one responsibility.
  • Each signal is idempotent or safe to run more than once.

If your signal starts turning into a 60‑line function with branching logic and network calls, that’s usually a sign it should be refactored into a service layer or task, with the signal doing nothing but calling that service.


FAQ: common questions about Django signals and real examples

What are some real‑world examples of Django signals being used well?

Good real‑world examples of Django signals include:

  • Creating related models like profiles or settings rows when a user is registered.
  • Writing audit log entries whenever sensitive models are created, updated, or deleted.
  • Enqueuing background tasks (emails, indexing, webhooks) on post_save.
  • Cleaning up files or cache entries on post_delete.

These align with the examples of Django signals: 3 practical examples shown earlier and scale reasonably well when kept small and focused.

Can you show an example of a custom Django signal?

Yes. Built‑in model signals are common, but you can define your own to decouple parts of your app:

## payments/signals.py
from django.dispatch import Signal

payment_succeeded = Signal()  # args passed via **kwargs

Emit it like this:

## payments/service.py
from .signals import payment_succeeded


def handle_payment_success(payment):
#    # business logic
    payment_succeeded.send(sender=type(payment), payment=payment)

And listen for it elsewhere:

## notifications/receivers.py
from django.dispatch import receiver

from payments.signals import payment_succeeded


@receiver(payment_succeeded)
def notify_on_payment(sender, payment, **kwargs):
#    # send email, push notification, etc.
    ...

This example of a custom Django signal is helpful when multiple apps care about the same event but shouldn’t know about each other directly.

Are Django signals bad for performance?

Signals themselves are just function calls, so they’re not inherently slow. Performance problems show up when receivers do heavy work synchronously: network calls, large queries, or expensive computations. The safer pattern, as shown in several examples of Django signals above, is to have receivers hand work off to background tasks (Celery, RQ, etc.) and keep the signal handlers thin.

How do I test code that uses Django signals?

You test the side effects the same way you test any other behavior:

  • Use factories to create or modify models.
  • Assert that the expected related objects, logs, or tasks exist.
  • When necessary, temporarily disconnect signals in tests using signals.post_save.disconnect(...) to isolate behavior.

Signals don’t need special treatment; they’re just another entry point into your business logic.


If you remember nothing else, remember this: the best examples of Django signals are boring. They’re short, predictable, and easy to forget about until you need them. Use them for cross‑cutting behavior like the three practical examples here, and your future self won’t hate you for it.

Explore More Django Code Snippets

Discover more examples and insights in this category.

View All Django Code Snippets