The Stack Evolution
Six packs from sync baseline to specialized infra — each one triggered by a specific pain point, never by chasing newness. Django + Next.js on a real stack progression.
There are two recurring failure modes in stack progression. The first is jumping ahead of the pain — adopting WebSockets when SSE would have done, or specialized infra when Postgres still had headroom. The second is staying too long on a pack that's no longer fit — workers thrashing, queues backing up, polling clients hammering endpoints that should have streamed.
This essay is the map of when to make each jump. Django + DRF on the back, Next.js 16 on the front. Six packs, each one triggered by a specific pain point — never by chasing newness. Stay on the lowest pack that solves the actual problem.
Sync Baseline
— one round trip, done —
↕ HTTP (request → response)
The contract is simple: client sends, server thinks, server replies, connection closes. Every framework feature is built around this loop — middleware, serializers, the ORM, all of it.
Beautiful while it lasts. Most CRUD apps never need to leave this pack.
Some work takes longer than a user will wait. The browser times out. The worker stays blocked. Some work needs to happen on a schedule.
Background Work
— punt slow stuff to a worker —
↕ HTTP + poll loop every 2s
Slow work moves out of the request path. The view dispatches a Celery task, returns a job id, and the frontend polls until status is done. Beat handles scheduled work — nightly digests, hourly cleanups, periodic syncs.
This is the workhorse setup for most SaaS. It will get you very far.
Polling is wasteful — most calls return nothing new. Sync views still block on slow external HTTP. We want to push live updates instead of asking for them.
Async + Streaming
— ASGI unlocks the long connection —
↕ HTTP + SSE stream (server pushes events)
Two new powers. First, async views: a worker that hits await httpx.get(...) sets the request aside and serves others until the response arrives. One worker juggles hundreds of concurrent slow I/O calls.
Second, streaming responses: yield SSE events as a Celery job progresses. The frontend opens one EventSource and receives "20% · 50% · done" pushes. The poll loop dies.
Same Django, same DRF — the protocol it speaks just changes from WSGI to ASGI underneath.
SSE is one-way. We need bidirectional — chat, multiplayer state, collaborative editing, presence indicators. Multiple clients syncing the same view.
WebSockets
— full duplex, persistent —
↕ WebSocket (persistent, full-duplex)
Channels extends Django past request/response. Consumer classes route WebSocket connections like URL routes; the channel layer (Redis) lets any process — another web worker, a Celery task, a management command — push a message into a specific socket or a group of them.
Daphne or Uvicorn — pick one. Daphne is Channels-native; Uvicorn is faster on plain HTTP. Both work.
One server isn't enough. Workers thrash. Postgres reads pile up. WebSocket reconnect storms during deploys. Time to spread out.
Scale-out
— horizontal everything —
The composition doesn't change much — the multiplicity does. Same components, more of them, with seams between them you can scale independently.
The two seams that matter most: queue priority split (so a slow ML job can't starve a fast email job) and Redis split (Celery's traffic profile and Channels' traffic profile are nothing alike — sharing one Redis is asking for noisy-neighbor pain).
The general-purpose stack stops being good enough for a specific workload. Postgres can't index your embeddings. The VPS disk is full of user uploads. ML inference is hot path.
Specialized Infra
— the right tool, where it belongs —
None of these are mandatory. Each appears when a specific workload outgrows the general-purpose stack. The art is not adopting them prematurely — every new piece of infra is a new operational surface.
The discipline carries across the whole evolution: each pack is a response to a real pain, never a response to wanting newer tools.
Adopt the lowest pack that solves your actual problem.
Most projects never need pack 04. Many teams adopt Channels and realize SSE was enough. The trigger is always pain, never novelty.