Welcome to my blog

Stop Django Bulk Operations From Silently Skipping Signals

Django’s bulk_create(), bulk_update(), and update() are fast because they skip save(). That also means they skip the pre_save and post_save signals, so your receivers never fire. It’s documented for bulk_create(), bulk_update(), and update(), but nothing warns you at runtime. If a signal invalidates a cache, writes an audit log, or syncs a search index, one bulk call quietly skips that critical work. This post shows how to make those calls fail loudly instead. ...

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

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