Django is loved for its “batteries-included” philosophy, and one of its most fascinating batteries is signals. They allow different parts of your app to talk to each other without being tightly coupled.
But like any hidden magic, signals can both save you time and cause painful debugging sessions if you’re not careful.
🎯 What Are Django Signals?
Think of a Django signal as a doorbell attached to your models:
Every time someone saves a
Product, the doorbell rings.Whoever is listening can react—without the
Productever knowing.
For example, a post_save signal can automatically log product changes:
@receiver(post_save, sender=Product)
def log_product_change(sender, instance, created, **kwargs):
action = "created" if created else "updated"
AuditLog.objects.create(
message=f"Product '{instance.name}' was {action}."
)
print(f"SIGNAL FIRED: Product '{instance.name}' was {action}.")
Now, every time a product is saved, an audit entry is logged.
🧩 Explicit vs. Implicit Logic
Here’s the big trade-off when using signals.
✅ Explicit Code (Service Function)
def create_product_with_audit(name, price):
product = Product.objects.create(name=name, price=price)
AuditLog.objects.create(
message=f"Product '{product.name}' was created via service."
)
print("SERVICE CALLED: Explicitly logged product creation.")
return product
You see exactly what happens: a product is created, then logged.
No hidden surprises.
⚡ Implicit Code (Signals)
product = Product.objects.create(name="Laptop", price=1500)
# Looks simple, right? But behind the scenes...
# -> AuditLog is created
# -> Maybe an email is sent
# -> Maybe a cache is cleared
# -> Maybe an API is called The .create() call looks harmless, but thanks to signals, it may trigger a cascade of invisible side-effects.
⚠️ Why Signals Can Burn You
At first, signals feel magical. But as your app grows, they can become a liability.
1. Hidden Costs
If a signal does heavy work (like a slow database query or external API call), it silently slows down every or call.
