Architecting a Django backend for 10,000+ daily users
Lessons from SabkiApp — keeping a media-heavy Django backend fast and predictable under real concurrency.
When a side feature becomes the product, your backend stops being a CRUD app and starts being a system. SabkiApp crossed that line somewhere past a few thousand daily users — and the lessons that mattered most were rarely the clever ones.
Measure before you optimize
The first rule was boring: instrument everything. Slow endpoints almost always traced back to a handful of N+1 queries hiding behind innocent-looking serializers.
# Before: one query per post for its author and media
posts = Post.objects.filter(feed=feed)
# After: the whole feed in two queries
posts = (
Post.objects.filter(feed=feed)
.select_related("author")
.prefetch_related("media")
)select_related and prefetch_related did more for tail latency than any caching layer I added later.
Cache the expensive, not the easy
I reserved caching for things that were genuinely costly to compute — aggregated feeds, permission trees — and gave each one a clear invalidation story. A cache you can't confidently invalidate is just a bug with a timer on it.
Make the database do the work
Pushing filtering, counting, and pagination down into Postgres — with the right indexes — beat pulling rows into Python every time. Keyset pagination kept deep scrolls cheap even as tables grew into the millions.
Takeaways
- Profile first; intuition lies about where the time goes.
- Treat every serializer as a potential N+1 until proven otherwise.
- Cache deliberately, with invalidation you trust.
- The fastest Python is the query you let Postgres run instead.