The Day We Lost Customer Data – A Django CASCADE Horror Story
Part 1: The Day We Lost Customer Data – A Django CASCADE Horror Story
It Started as a Routine Cleanup
I was performing standard database maintenance on our e-commerce platform. We had accumulated thousands of test orders over months of development, and it was time to clean them up.
1
2
# The innocent-looking command that started it all
Order.objects.filter(is_test=True).delete()
Always check relationships before mass deletions. Run Order._meta.get_fields() to see all related models.
The Moment Panic Set In
Within 11 seconds (we timed it later):
- 📞 23 customer support calls about missing orders
- 💸 $18,200 in processed payments disappeared from reports
- 📉 Dashboard showed 50,382 records vaporized
CASCADE deletions are atomic - once started, they can’t be stopped mid-execution!
How a Simple Delete Became a Disaster
The Silent Data Killer: on_delete=CASCADE
1
2
3
4
5
6
7
8
9
# Our dangerous relationship definitions
class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)  # 🚨
class Payment(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)  # 💥
class Refund(models.Model):
    payment = models.ForeignKey(Payment, on_delete=models.CASCADE)  # 🔥
The cascade effect timeline:
- 00:00:00 - 1,237 test orders deleted
- 00:00:03 - 8,642 related payments gone
- 00:00:07 - 12,109 refunds vanished
- 00:00:11 - 28,394 audit logs erased
Django doesn’t show deletion previews. Use queryset.count() and collector.collect() to estimate impact.
What We Didn’t Anticipate
- Reverse relations: Customer.order_setwas being used in analytics
- M2M through models: Order.tagshad a hidden cascade effect
- Signals: post_deletehandlers were archiving data (then deleting it)
First Response: Damage Assessment
- Emergency protocol:1 ./manage.py maintenance_mode on # Custom command
- Backup verification:1 SELECT pg_size_pretty(pg_database_size('backup_db')); # 42GB - good 
- Forensic analysis:1 2 from django.db.models.signals import pre_delete print(connections['default'].queries[-20:]) # See recent SQL 
Keep a read-only replica for forensic analysis during disasters.
Why We Couldn’t Just Restore From Backup
| Option | Risk | Time Estimate | 
|---|---|---|
| Full restore | Lose 1 hour of real orders | 2+ hours | 
| Partial restore | FK violations | Unknown | 
| Surgical recovery | Complex but precise | 90 minutes | 
PostgreSQL’s pg_restore --data-only still checks constraints!
Coming in Part 2: The Surgical Recovery
What to expect:
- 💻 Creating a parallel recovery environment
- 🔎 Using Django’s deletion.Collectorproperly
- ⚡ Batch processing with iterator()
- � Handling M2M relationships
- ✅ Post-recovery verification scripts
Key Lesson:
1
2
3
4
# The fix we implemented later
class Payment(models.Model):
    order = models.ForeignKey(Order, on_delete=models.PROTECT)  # 🛡️
    # ...
💬 Discussion
Have you experienced cascade disasters? What safeguards do you use? Share below!