What This Article Is About
HTTP is request-response. The browser asks, the server answers. But many modern features need the opposite: the server has news the browser needs to know. A new chat message arrived. A stock price changed. An AI is streaming back a response token by token. A teammate just edited the same document.
There are four main patterns for "server-initiated" updates to a browser, plus a couple of newer ones gaining traction. They differ in latency, complexity, infrastructure cost, and what they can actually do. Picking the right one is mostly about understanding the trade-offs.
This article walks through every option: short polling, long polling, Server-Sent Events (SSE), WebSockets, and the newer WebTransport. We'll cover how each works, when each makes sense, the gotchas at scale, and why SSE is the underrated right answer for most "live updates" use cases.
The Mental Model: Who Owns the Connection?
Every real-time technique answers the same question: "if the server has news, how does it deliver it to the browser without the browser asking first?"
The strategies cluster around two ideas:
The browser keeps asking. Pull. Polling. Cheap to build, expensive to operate. Updates are at most N seconds late.
A connection stays open. Push. The connection lives between requests, and the server uses it to deliver events as they happen. Lower latency, but the server has to handle many open connections at once.
Once you see real-time as "polling vs persistent connection", the rest is just details about HOW the persistent connection is structured.
1. Short Polling
The browser asks every N seconds: "anything new?" Most of the time the answer is "no".
// Browser
setInterval(async () => {
const res = await fetch('/api/messages?since=' + lastSeenId);
const messages = await res.json();
if (messages.length) showMessages(messages);
}, 5000);
How it actually works: a normal HTTP request every 5 seconds (or whatever interval). The server queries the DB for changes since the last seen ID. If nothing changed, it returns an empty array. If something changed, it returns the new items.
Pros:
Trivially simple. Works with any HTTP server, any browser, any proxy.
Stateless on the server side. Server doesn't need to remember which clients are connected.
Cleanly load-balanced; any server can handle any request.
Easy to debug. Just normal HTTP.
Cons:
Wasteful. 95% of requests return nothing useful.
High latency. Updates are at most N seconds late.
Server load scales with users * polling rate. 100k users polling every 5 seconds = 20k requests/second of pure overhead.
Battery drain on mobile.
Use it when: updates are rare (a few per hour), latency tolerance is high (minutes), or simplicity matters more than efficiency. Internal admin dashboards, sometimes notifications.
2. Long Polling
Browser sends a request. Server holds it open without responding. When something happens, the server returns the response. Browser immediately makes another request to wait for the next event.
// Browser
async function listen() {
while (true) {
const res = await fetch('/api/wait-for-message');
const event = await res.json();
handleEvent(event);
// Loop continues, opens next request
}
}
How it works: the server doesn't return immediately. It blocks (or waits asynchronously) until either an event arrives or a timeout (typically 30-60 seconds). When the response is sent, the client immediately starts a new request.
The connection is open most of the time, but events get delivered with sub-second latency.
Pros:
Works over plain HTTP. Any proxy, firewall, or load balancer that handles HTTP works.
Near-real-time delivery when events arrive.
Stateless-ish (each request is its own thing; the wait is server-side state).
Cons:
Each event is a full HTTP request and response cycle. Connection churn.
Servers must hold many open connections concurrently. Traditional thread-per-request servers can't scale (10k threads for 10k waiting clients).
Race conditions when events arrive between connections.
Modern alternatives (SSE, WebSockets) do this better in almost every way.
Use it when: a constraint forces HTTP-only and you need lower latency than short polling. Mostly a legacy choice; new code rarely picks long polling.
3. Server-Sent Events (SSE)
One HTTP connection, kept open, server streams events down it forever. The browser uses the EventSource API to read them. Standardized, simple, built into every modern browser.
// Browser
const events = new EventSource('/events');
events.onmessage = (e) => console.log(e.data);
events.onerror = () => console.log('disconnected, will auto-reconnect');
// Server response (Content-Type: text/event-stream)
data: {"type": "new_message", "id": 42}
data: {"type": "price_update", "value": 99.99}
data: {"type": "ping"}
The format is dead simple: lines starting with data: become events. Blank line separates events. The browser delivers each event to onmessage.
How it works: regular HTTP request that the server never closes (or keeps reopening). The response uses Transfer-Encoding: chunked or HTTP/2 streaming. The browser keeps reading the response body indefinitely.
Pros:
Standard HTTP. Works with HTTP/1.1, HTTP/2, HTTP/3. Most proxies pass it through.
Auto-reconnect built into EventSource. If the connection drops, the browser tries again automatically. The server can include event IDs and the browser sends Last-Event-ID on reconnect, so the server can resume.
Simpler than WebSockets. No special protocol upgrade, no special framing, no client library needed.
Works through corporate firewalls and proxies (it's just HTTP).
Plays well with HTTP/2 multiplexing (no per-connection cost beyond a stream).
Cons:
One direction only: server to client. If the client also needs to send messages, use a separate POST.
Text-only. The format is line-based. Binary requires base64 (which is fine for small payloads, awkward for large).
Browser limits the number of EventSource connections per origin (6 in most browsers, but more on HTTP/2 due to multiplexing).
Some load balancers buffer responses by default and break SSE; you have to disable buffering on the SSE path.
Use it when: you only need server-to-client streaming. Live notifications, dashboards, status pages, AI token streaming (ChatGPT and Claude both use SSE for streaming responses), live counters, log tailing.
4. WebSockets
Full bidirectional persistent connection. Starts as HTTP and upgrades to the WebSocket protocol via the Upgrade: websocket header. After the upgrade, both sides can send messages whenever they want.
// Browser
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => ws.send('hello');
ws.onmessage = (e) => console.log(e.data);
ws.onclose = () => reconnectWithBackoff();
How it works: the client sends a normal HTTP request with special headers (Upgrade: websocket, Sec-WebSocket-Key). If the server agrees, it responds with 101 Switching Protocols. The TCP connection is reused but no longer speaks HTTP. From this point, both sides exchange WebSocket frames.
Frames have small headers (a few bytes), can be text or binary, can be fragmented for large messages, and include ping/pong frames for keepalives.
Pros:
Truly bidirectional. Both sides can initiate messages.
Lowest per-message overhead after the initial handshake. A few bytes of frame header per message.
Binary data is native. Send a Uint8Array directly.
Real-time. The connection never closes; the latency is just the network RTT.
Mature ecosystem (Socket.IO, ws, gorilla/websocket, etc.).
Cons:
Not HTTP after the upgrade. Some proxies, load balancers, and intrusion detection systems handle it badly. They might close idle connections, buffer, or drop the upgrade.
No standardized auto-reconnect. Each application implements its own logic (with backoff, etc.).
More complex than SSE. You build the protocol on top: message types, request IDs, error handling, state recovery.
Stateful. The connection is tied to a specific server instance. Load balancing across many servers needs sticky sessions or a pub/sub layer underneath.
Authentication is harder. You can't set a token in headers after upgrade. Common workarounds: send a token in the initial query string, send an auth message after connection, or use cookies.
Use it when: you need true bidirectional real-time. Chat, multiplayer games, collaborative editing (Google Docs, Figma), live trading, IoT control.
5. WebTransport (The New Kid)
WebTransport is an emerging API based on HTTP/3 and QUIC. It gives the browser something WebSockets can't easily do: unreliable datagrams (UDP-like) plus reliable streams, all multiplexed on one connection.
Browser support is partial (Chrome and Edge in 2026; Firefox and Safari evolving). Use cases overlap with WebSockets but extend to:
Real-time games where some packets can be dropped (position updates).
Low-latency video, audio.
Anything that benefits from multiple independent streams without head-of-line blocking.
Not yet a default choice. Watch this space; for most current production work, WebSockets and SSE are the answer.
Side-by-Side Comparison
| Short Polling | Long Polling | SSE | WebSockets | |
|---|---|---|---|---|
| Direction | Client to server | Client to server | Server to client | Bidirectional |
| Latency | High (~N seconds) | Low | Low | Lowest |
| Connection cost | High (per request) | High (open + reopen) | Low (one stream) | Lowest |
| Reconnection | N/A | Manual | Auto (built-in) | Manual (DIY) |
| Binary data | Yes (HTTP) | Yes | No (text only) | Yes |
| Plays well with HTTP/2 | Yes | Yes | Excellent (multiplexed) | Limited (RFC 8441) |
| Through corp proxies | Always | Always | Usually | Sometimes |
| Server complexity | Trivial | Medium (long-running) | Medium | High |
| Auth via headers | Yes | Yes | Yes (initial req) | Awkward (initial req only) |
How to Decide
A practical decision tree:
Need bidirectional real-time? WebSockets. Chat, games, collaborative editing.
Server-to-client only, simple delivery? SSE. Underrated and probably right for most "live updates" cases. Notifications, dashboards, AI streaming.
Updates rare, latency okay (minutes)? Polling. Cron-style refreshes, internal tools.
Forced HTTP-only, need lower latency than polling? Long polling. Mostly legacy.
P2P browser-to-browser (video, voice)? WebRTC. Different problem.
The most common mistake: jumping to WebSockets when SSE would do. WebSockets are more powerful but bring real complexity (reconnection, auth, sticky sessions, proxy compatibility). If you only need server-to-client, SSE is simpler and gives auto-reconnect for free.
Operational Concerns
Connection count. WebSockets and SSE keep one connection open per user. A million users = a million open connections. This requires event-loop-based servers (Node.js, Go, async Python with asyncio, Java with virtual threads, Rust with tokio). Traditional thread-per-connection servers (legacy Tomcat, blocking Python WSGI) don't scale to that.
Memory per connection. A pure WebSocket connection in Node or Go takes a few KB. With application state (subscriptions, buffers), maybe 10-50 KB. Plan for total memory = users * per-connection-overhead.
Load balancing and stickiness. If user A's connection is on server 1 and an event needs to reach user A, server 1 must know. Two patterns:
Sticky sessions: load balancer always sends user A to server 1. Then the app server has direct access to A's connection.
Pub/sub backplane: every server subscribes to a Redis (or NATS, or Kafka) channel. When an event needs to reach a user, you publish to that channel. All servers receive it; whichever has the user's connection delivers it.
Pub/sub is more flexible (any server can serve any user). Sticky is simpler but limits scaling. Most large apps use both: sticky to keep things simple, pub/sub for cross-server fanout.
Heartbeats. NATs, firewalls, and load balancers drop "idle" connections after some timeout (often 30-60 seconds). Send a tiny ping every 15-30 seconds to keep the connection alive. WebSockets have native ping/pong frames; SSE uses comment lines (: keepalive\n\n).
Reconnection storm. If your server restarts, every connected client immediately reconnects. Without backoff and jitter, this is a thundering herd. Implement exponential backoff with random jitter. SSE's auto-reconnect uses a configurable interval.
Backpressure. If a client is slow to read (poor network, busy CPU), and you keep sending events, the server's send buffer fills up. Eventually you have to either drop events for that client or force-disconnect them. Decide explicitly which.
Resume semantics. After a disconnect, what happens to events the client missed? SSE has Last-Event-ID for this. WebSockets are application-level; you have to design a "give me everything since X" protocol.
Authentication. The initial HTTP request can carry cookies or Authorization headers. After WebSocket upgrade, you can't set headers, so token-in-query-string or auth-message-after-connect are common patterns. Be careful with token-in-query-string and logging (tokens shouldn't appear in access logs).
CORS. WebSockets have their own CORS-like model (the Origin header is the main check). SSE follows normal HTTP CORS. Plan for both.
Patterns at Scale
Pub/sub backplane: Redis pub/sub, NATS, or a Kafka-fed fanout layer. Decouples event producers from connection-holding servers.
Stateless dispatch with topic-aware routing: the connection-holding servers maintain a map "user X is connected on this server". When publishing, you can route directly. Many systems use consistent hashing of user IDs to a server.
Hosted services: Pusher, Ably, AWS API Gateway WebSocket, Pusher Channels. They handle the connection-holding tier so you don't have to. Useful when scaling is the bottleneck.
Server-Sent Events on HTTP/2: a single TCP connection can carry many SSE streams (one per HTTP/2 stream). Per-user resource cost shrinks dramatically. This is one reason SSE became more attractive after HTTP/2.
Edge Cases and Gotchas
Buffering proxies break SSE. Many proxies (nginx by default, some CDNs, some corporate proxies) buffer responses to improve throughput. SSE breaks because the events arrive in batches instead of streaming. Disable proxy buffering on the SSE path (proxy_buffering off in nginx, X-Accel-Buffering: no response header).
WebSockets through corporate firewalls. Some firewalls block the WebSocket upgrade or drop the connection. Mitigation: connect over wss:// (port 443) so it looks like normal HTTPS. Even then, deep-packet-inspecting firewalls might break it.
Browser tab in background. Browsers throttle background tabs. Setintervals fire less often. WebSocket and SSE connections may be paused or rate-limited. Don't assume real-time delivery while the tab is hidden.
Mobile networks. Aggressive NAT timeouts, frequent network changes (WiFi to cellular). Heartbeats and quick reconnect logic are critical. SSE's built-in reconnect helps.
Maximum WebSocket message size. Most servers limit individual messages (often 1-16 MB default). Anything larger should be chunked into multiple messages.
SSE event-id wrap-around. If you use Last-Event-ID for resume, the server must remember enough history to satisfy a reconnecting client. Otherwise the client misses events. Bound the buffer; for a permanent miss, force a full refresh.
Cookie auth and WebSocket Origin. Browsers send cookies with the WebSocket upgrade by default. If the upgrade succeeds with an attacker's site as Origin, you have CSRF-style risk. Always validate the Origin header server-side.
Server restarts disconnect everyone. A rolling deploy kicks every client off. Clients reconnect. If you want zero-disruption deploys, you need session migration tooling (rare; most apps just accept brief reconnects).
Don't Forget WebRTC
If your use case is browser-to-browser peer-to-peer (video calls, voice, file transfer, screen sharing), WebRTC is the right tool. It's UDP-based, with NAT traversal (STUN/TURN), built-in encryption, and a media engine for audio/video.
WebRTC is conceptually different from WebSockets/SSE: those are client-to-server, WebRTC is browser-to-browser. They're complementary; many apps use a WebSocket for signaling (exchanging the metadata to set up the WebRTC connection) and WebRTC for the actual media.
The One Thing to Remember
For most "the server has news" use cases, SSE is the underrated right answer. Simpler than WebSockets, more efficient than polling, built into browsers, automatic reconnect, plays nicely with HTTP/2. Reach for WebSockets when you need true bidirectional traffic (chat, games, collaborative editing). Use polling when updates are rare and simplicity wins. Long polling only when legacy constraints force HTTP-only and you need low latency. The biggest mistake is jumping to WebSockets out of habit when SSE would have been half the code with twice the reliability. Pick the protocol that matches your direction (one-way vs two-way) and your scale; let everything else stay boring.