Back to Notes

Communication Protocols (HTTP Polling, Long Polling, WebSockets, SSE, gRPC)

The Question Behind the Question

"How does the client get real-time updates?" is really "what's the trade-off between connection overhead, latency, and infrastructure complexity you're willing to accept?" Every chat/notification/live-feed problem hinges on this choice.

Short (HTTP) Polling

Client asks "anything new?" on a fixed interval, regardless of whether there's anything new.

import time, requests

def poll(url, interval_s=3):
    while True:
        resp = requests.get(url)
        if resp.json().get("has_update"):
            handle(resp.json())
        time.sleep(interval_s)

Cons: wastes requests when nothing changed, and update latency is bounded by the interval (average delay = interval/2). Pros: trivial to implement, works everywhere, no persistent connection to manage.

Long Polling

Client asks "anything new?" — server holds the request open until there's an update (or a timeout), then responds; client immediately re-requests.

import requests

def long_poll(url, timeout_s=30):
    while True:
        resp = requests.get(url, params={"timeout": timeout_s})
        if resp.status_code == 200:
            handle(resp.json())
        # else: timed out with no update, just re-request

Pros: much lower latency than short polling, no new protocol needed (still plain HTTP). Cons: server must hold open connections/threads for every waiting client — resource cost scales with concurrent idle clients, a real capacity concern at scale.

WebSockets

A single TCP connection is upgraded from HTTP to a persistent, full-duplex channel — either side can push a message at any time, no request/response ceremony per message.

import asyncio, websockets

async def handler(websocket):
    async for message in websocket:
        await websocket.send(f"echo: {message}")   # push anytime, either direction

async def main():
    async with websockets.serve(handler, "localhost", 8765):
        await asyncio.Future()  # run forever

Pros: lowest latency, true bidirectional push — the right choice for chat, live collaborative editing, multiplayer state sync. Cons: stateful connection per client (doesn't fit cleanly behind simple round-robin load balancers — needs sticky routing or a connection-aware gateway), harder to scale horizontally (a message to a user must be routed to whichever server holds their socket — typically solved with a pub/sub layer like Redis pub/sub or Kafka fanning out to the right server).

Server-Sent Events (SSE)

One-directional push, server → client only, over plain HTTP (no upgrade needed).

Pros: simpler than WebSockets when the client never needs to send data back over the same channel (live score updates, notification streams) — auto-reconnect built into the browser EventSource API. Cons: one-directional only; client writes still need a separate normal HTTP request.

gRPC

RPC framework over HTTP/2, with strongly-typed contracts (Protobuf) and support for streaming (client, server, or bidirectional).

Pros: efficient binary serialization, multiplexed streams over one connection, strong typing catches contract mismatches at compile time — the default choice for service-to-service internal communication at scale. Cons: not natively browser-friendly (needs gRPC-Web + a proxy translation layer), less human-debuggable than JSON over REST.

Decision Table

NeedChoice
Occasional updates, simplicity over efficiencyShort polling
Lower latency, no new infra, moderate concurrencyLong polling
True real-time, bidirectional, high concurrencyWebSockets (+ pub/sub fanout)
Server → client stream only, want auto-reconnectSSE
Internal service-to-service, high throughput, typed contractsgRPC

Interview Drill

Q: You're designing WhatsApp-style chat. A user has the app open on 3 devices. A message arrives. Walk through delivery. A: The sending client's message hits the chat service, which persists it, then needs to push to all 3 of the recipient's active WebSocket connections — which may be held by 3 different server instances (connections aren't sticky to one device necessarily, but each device does hold its own persistent socket). Use a pub/sub layer (Redis pub/sub or a Kafka topic keyed by user_id) so any server instance that publishes "new message for user X" gets fanned out to whichever instances currently hold that user's live sockets. This is the concrete reason WebSockets don't scale horizontally "for free" the way stateless HTTP does — you need this extra fanout layer.

Cross-links

[[Load Balancing]] · [[Message Queues & Kafka]] · [[API Design Principles]]