Post

The Day We Lost Customer Data – A Django CASCADE Horror Story

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()

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

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:

  1. 00:00:00 - 1,237 test orders deleted
  2. 00:00:03 - 8,642 related payments gone
  3. 00:00:07 - 12,109 refunds vanished
  4. 00:00:11 - 28,394 audit logs erased

What We Didn’t Anticipate

  • Reverse relations: Customer.order_set was being used in analytics
  • M2M through models: Order.tags had a hidden cascade effect
  • Signals: post_delete handlers were archiving data (then deleting it)

First Response: Damage Assessment

  1. Emergency protocol:
    1
    
    ./manage.py maintenance_mode on  # Custom command
    
  2. Backup verification:
    1
    
    SELECT pg_size_pretty(pg_database_size('backup_db'));  # 42GB - good
    
  3. Forensic analysis:
    1
    2
    
    from django.db.models.signals import pre_delete
    print(connections['default'].queries[-20:])  # See recent SQL
    

Why We Couldn’t Just Restore From Backup

OptionRiskTime Estimate
Full restoreLose 1 hour of real orders2+ hours
Partial restoreFK violationsUnknown
Surgical recoveryComplex but precise90 minutes

Coming in Part 2: The Surgical Recovery

What to expect:

  1. 💻 Creating a parallel recovery environment
  2. 🔎 Using Django’s deletion.Collector properly
  3. ⚡ Batch processing with iterator()
  4. � Handling M2M relationships
  5. ✅ 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!


Continue Reading in Part 2

The Surgical Data Recovery - How We Restored 50,382 Records

This post is licensed under CC BY 4.0 by the author.