Real-Time Delivery

Pushing data to clients instead of waiting for them to ask โ€” and the stateful-connection problems that come with it.

Long polling SSE WebSockets Connection state Fan-out at scale
๐Ÿชœ

The Options Ladder

The web is pull-based: clients ask, servers answer. Real-time delivery is the set of tricks for reversing that, so the server can push the moment something changes. There are four rungs, each one closer to a true push channel and each one more stateful than the last.

  SHORT POLLING          client asks every N seconds, mostly gets "nothing yet"
    C โ”€reqโ”€โ–ถ S  (empty)   โ”  simple, works everywhere
    C โ”€reqโ”€โ–ถ S  (empty)   โ”ผ  wasteful; latency = poll interval
    C โ”€reqโ”€โ–ถ S  (data!)   โ”˜

  LONG POLLING           client asks, server HOLDS the request until data exists
    C โ”€reqโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ S    โ”  near-real-time on plain HTTP
              (waits)     โ”ผ  one connection per message; reconnect churn
    C โ—€โ”€โ”€dataโ”€โ”€ S  reopen โ”˜

  SERVER-SENT EVENTS     one long-lived HTTP stream, server pushes many events
    C โ”€reqโ”€โ–ถ S            โ”  server โ†’ client only, auto-reconnect built in
    C โ—€โ•eventโ• S          โ”ผ  text only, rides plain HTTP
    C โ—€โ•eventโ• S          โ”˜

  WEBSOCKETS             upgrade HTTP once, then a full-duplex TCP channel
    C โ‡„ handshake โ‡„ S     โ”  bidirectional, low overhead per message
    C โ‡„โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ‡„ S      โ”ผ  stateful, needs its own scaling story
    (both push anytime)   โ”˜
โ„น๏ธ
The jump that matters is from polling to persistent connections. Polling reuses the stateless request-response model, so it scales like any HTTP endpoint. SSE and WebSockets hold a connection open per client, which is where all the hard problems below start.
๐ŸŽฏ

Choosing Between Them

Reach for the simplest rung that meets the requirement. Three questions settle it: which direction does data flow, how often does it update, and how much connection machinery are you willing to run.

TechniqueDirectionLatencyCost / complexityBest fit
Short pollingClient pull= poll intervalTrivial, but wastefulRare updates, small scale, quick hacks
Long pollingServer โ†’ clientNear real-timeReconnect churnNotifications where WebSockets are overkill
SSEServer โ†’ clientReal-timeLow, auto-reconnectFeeds, live scores, dashboards, notifications
WebSocketsBidirectionalReal-timeHigh, statefulChat, games, collaborative editing, trading
๐Ÿ’ก
SSE is the underused answer. If the client only needs to receive โ€” a live feed, price ticks, a progress bar โ€” SSE gives you real-time push over plain HTTP with reconnection handled for you, and none of the WebSocket scaling burden. Don't default to WebSockets just because it's the one everyone names; reach for it when you genuinely need the client to push too (Chat System).
๐Ÿ“Œ

Connection State Is the Hard Part

A stateless HTTP server is easy: any box handles any request. A persistent connection breaks that. The client is now bound to one specific server for the life of the connection, and the system has to answer questions it never had to before.

Which server holds this user? routing to a connection

To push to Bob, you must find the exact server holding Bob's socket. Two options: sticky routing (a load balancer pins Bob to one node) or a connection registry (a shared store mapping user โ†’ server, so any node can look up where to send).

Is the connection still alive? dead-connection detection

TCP can stay "open" long after the client vanished (laptop closed, tunnel dropped). Heartbeats โ€” periodic pings โ€” detect the dead ones so you free memory and stop pushing into the void.

Reconnect and resume picking up where it dropped

Connections drop constantly โ€” phones change networks, servers deploy. The client reconnects with exponential backoff and jitter so a mass disconnect doesn't become a synchronized reconnect stampede (Resilience Patterns). On reconnect it sends a last-event-id so the server can replay whatever was missed during the gap, rather than losing it.

โš ๏ธ
Sticky routing is the tempting shortcut and a trap at scale: it undermines even load balancing and turns any deploy into a mass reconnect, because every connection on a restarting node drops at once. A connection registry plus a fan-out backbone scales better, at the cost of a lookup on every push.
๐Ÿ“ˆ

Scaling to a Million Connections

Each open connection costs memory (buffers, TLS state โ€” order of tens of KB), so a million connections is real RAM and file-descriptor pressure. The standard shape separates the connection-holding tier from everything else, and uses a pub/sub backbone to fan messages out to whichever gateway holds the target.

   millions of clients
        โ”‚  โ”‚  โ”‚  (WebSocket / SSE)
        โ–ผ  โ–ผ  โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   Connection gateways: a dedicated tier
  โ”‚  GW-1     GW-2     GW-3   ...  โ”‚   whose only job is holding sockets.
  โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   Scale this independently of app logic.
      โ”‚         โ”‚         โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             Pub/sub backbone (Redis / Kafka):
        โ”‚   pub / sub    โ”‚            a message for user X is published;
        โ”‚   backbone     โ”‚            the gateway holding X is subscribed
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             and delivers it. No gateway-to-gateway
                โ–ผ                      mesh needed.
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  app services  โ”‚            Business logic stays stateless behind
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             the gateway tier.

The gateways become stateless in the way that counts: they hold connections but no business logic, so you can add nodes freely. On deploy, you drain a gateway โ€” stop new connections, let clients reconnect elsewhere with backoff โ€” instead of dropping everything at once (Scaling Patterns).

โœ…

Delivery Over Flaky Links

A pushed message can vanish: the connection dropped between send and receive, and the server never knew. "I sent it" is not "they got it." Guaranteeing delivery over an unreliable link needs three pieces.

PieceWhat it does
Client acksThe client confirms receipt; unacked messages are considered undelivered and eligible for resend.
Server-side bufferHold recent messages per client until acked, so a reconnecting client can be replayed from where it left off.
Per-channel orderingSequence numbers within a conversation or topic, so the client can order and dedup even if delivery is out of order.
โ„น๏ธ
Ack-plus-resend means a message can arrive twice โ€” the ack got lost, so the server resends something the client already had. That's at-least-once delivery, and the fix is the same everywhere: the client dedups on message ID, making reprocessing harmless (Messaging, Concurrency Control).
๐Ÿ“‹

Quick Reference

TechniqueLatencyDirectionCostBest fit
Short polling= intervalClient pullTrivial, wastefulRare updates, small scale
Long pollingNear real-timeServer โ†’ clientReconnect churnSimple notifications
SSEReal-timeServer โ†’ clientLow, auto-reconnectFeeds, dashboards, live scores
WebSocketsReal-timeBidirectionalHigh, statefulChat, games, collaboration
โ„น๏ธ
The one-liners to keep ready. Pick the lowest rung that fits: SSE for server-push-only, WebSockets only when the client must push too. Persistent connections make servers stateful โ€” solve it with a connection registry and a pub/sub backbone, not sticky routing. Scale connections in a dedicated gateway tier you can drain on deploy. Guarantee delivery with client acks, a server buffer, and per-channel sequence numbers, and dedup on the client because delivery is at-least-once.