Welcome to my blog

Caching at Scale in Django: Lessons from Production

My team runs a multi-tenant Django API that serves browser extensions. Traffic arrives in waves: a company’s employees all log on in the same minute, or a customer pushes an update to every user at once. Traffic grew about 20x in two years. The API peaks around 1,100 requests per second on a typical weekday. The busiest moment we have seen, a large customer pushing an update to all of its users at once, hit 2,000. Postgres sees about 0.3 queries per request. Redis serves the rest at a 97% hit rate. Caching was the biggest lever, and here is what we learned. ...

October 25, 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

Avoiding Duplicate Objects in Django Querysets

When filtering Django querysets across relationships, you can easily end up with duplicate objects in your results. This is a common gotcha that happens with both one-to-many (1:N) and many-to-many (N:N) relationships. Let’s explore why this happens and the best way to avoid it. The Problem When you filter a queryset by traversing a relationship, Django performs a SQL JOIN. If a parent object has multiple related objects that match your filter, the parent object appears multiple times in the result set. ...

January 3, 2026

7 Heroku Features Every Developer Should Be Using

Heroku makes hosting simple, but most developers overlook its advanced capabilities. After years of running apps on the platform, I’ve found several underrated features that speed up your workflow and make your apps more reliable. Here are seven you shouldn’t miss. 1. Preboot Preboot spins up new dynos and starts routing traffic to them before the old ones are terminated. The result is seamless, zero-downtime deployments. The tricky part is knowing when the cutover is complete, since the Heroku dashboard doesn’t show it. Use this command to confirm when the new dynos are live: ...

September 13, 2025