Welcome to my blog

Prevent Rolling Deploys From Breaking Your Django Frontend

During a rolling deploy, new HTML can ask an old server for files it doesn’t have. Fix it in Django with shared storage and a CDN for zero-downtime deploys.

November 15, 2026

Caching at Scale in Django: Lessons from Production

What we learned caching a busy Django API in Redis: cache shared data once, invalidate exactly what changed, and treat Redis like critical infrastructure.

November 1, 2026

Stop Django Bulk Operations From Silently Skipping Signals

Django’s bulk_create(), bulk_update(), and update() silently skip save signals. Make them fail loudly with a custom QuerySet and an explicit opt-out.

October 27, 2026

Django Custom Managers Are Silently Leaking Data

Django custom managers are a common way to exclude rows by default, such as inactive or soft-deleted rows. However, they don’t run everywhere you’d expect, which leads to unintended data exposure. This post covers where that happens and how to fix it. The Setup Let’s model stores and products with a soft-deletable relationship: class Store(models.Model): name = models.CharField(max_length=255) class Product(models.Model): name = models.CharField(max_length=255) class StoreProductManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(active=True) class StoreProduct(models.Model): store = models.ForeignKey(Store, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) objects = StoreProductManager() # default: active only all_objects = models.Manager() # escape hatch: everything The custom manager is declared first, making it the default manager. That feels like it should protect us everywhere, but it doesn’t. ...

March 22, 2026

Django Time-Based Lookups: A Performance Trap

Django’s field lookups are one of the ORM’s best features, but time-based lookups can quietly bypass database indexes, turning fast queries into expensive full table scans. A Slow Production Query I ran into this while debugging a 30 second query on a large table (~25 million rows): class Event(models.Model): timestamp = models.DateTimeField(db_index=True) Event.objects.filter(timestamp__date=datetime.date(2026, 1, 5)).count() It generates SQL like this: SELECT COUNT(*) FROM event WHERE timestamp::date='2026-01-05'; At first glance, it looked totally reasonable — a simple filter on an indexed field. But after checking the query plan, I discovered the issue: the query can’t use the index and falls back to a full table scan because it casts the field to a date. ...

January 19, 2026