All posts
1 min read

Architecting a Django backend for 10,000+ daily users

Lessons from SabkiApp — keeping a media-heavy Django backend fast and predictable under real concurrency.

DjangoBackendScalingPostgreSQL

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.