Polling is a pull model where clients ask repeatedly; SSE and WebSockets hold one connection open so the server can push in real time. Choosing among them is really three questions — which direction does data flow, how often does it change, and what infrastructure sits in between — and the honest answer differs per feature, not per app.
Key takeaways
| Question | Answer |
|---|---|
| Short polling | Ask on a timer — simple, cache-friendly, mostly wasted requests |
| Long polling | Server holds the request until data arrives — push simulated over plain HTTP |
| SSE | One HTTP stream, server → client text events, auto-reconnect built in |
| WebSockets | One socket, full-duplex, binary-capable — you own the protocol |
| The rule of thumb | One-way → SSE · two-way → WebSockets · rare changes → polling is fine |
The code, side by side
// 1 · Short polling — ask on a timer
setInterval(async () => {
const res = await fetch('/api/messages?since=' + lastId);
render(await res.json()); // usually empty — headers paid anyway
}, 2000);
// 2 · Server-Sent Events — one HTTP stream, server pushes text events
const events = new EventSource('/api/stream');
events.onmessage = (e) => render(JSON.parse(e.data)); // auto-reconnects
// 3 · WebSocket — one socket, both directions
const ws = new WebSocket('wss://api.example.com/live');
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.send(JSON.stringify({ type: 'typing' })); // client pushes too
What most apps actually ship — a subscription layer that owns the transport for you:
// JavaScript / Node.js — Back4app JS SDK
// The polling replacement: subscribe once, receive pushes
const query = new Parse.Query('Message');
query.equalTo('room', 'general');
const subscription = await query.subscribe(); // WebSocket under the hood
subscription.on('create', (msg) => render(msg)); // pushed, not polled
// subscription.unsubscribe() when the screen closes // Flutter / Dart — Back4app Flutter SDK
// The polling replacement: subscribe once, receive pushes
final liveQuery = LiveQuery();
final query = QueryBuilder<ParseObject>(ParseObject('Message'))
..whereEqualTo('room', 'general');
final subscription = await liveQuery.client.subscribe(query); // WebSocket under the hood
subscription.on(LiveQueryEvent.create, (msg) => render(msg)); // pushed, not polled // iOS / Swift — Back4app Swift SDK
// The polling replacement: subscribe once, receive pushes
let query = Message.query("room" == "general")
let subscription = try await query.subscribe() // WebSocket under the hood
subscription.handleEvent { _, event in
if case .created(let msg) = event { render(msg) } // pushed, not polled
} // Android / Kotlin — Back4app Android SDK
// The polling replacement: subscribe once, receive pushes
val client = ParseLiveQueryClient.Factory.getClient()
val query = ParseQuery.getQuery<ParseObject>("Message")
query.whereEqualTo("room", "general")
val subscription = client.subscribe(query) // WebSocket under the hood
subscription.handleEvent(SubscriptionHandling.Event.CREATE) { _, msg ->
render(msg) // pushed, not polled
} How each technique works
Short polling is a setInterval around a fetch: ask every n seconds whether anything changed. Every cycle pays a full HTTP request/response — headers, auth, routing — usually to hear “nothing yet,” and average delivery delay is half the interval plus a round trip.
Long polling moves the waiting server-side: the client asks, the server holds the request open until data arrives or a timeout fires, the client immediately re-asks. It approximates push over vanilla HTTP — the Comet-era workaround, with its known costs (reconnect churn, per-request headers, ordering care) cataloged in RFC 6202.
Server-Sent Events make the held response permanent: one HTTP response with Content-Type: text/event-stream that never ends, down which the server writes UTF-8 events (data:, event:, id:, retry: fields, per the WHATWG standard). The browser’s EventSource consumes it — and reconnects automatically, resuming via Last-Event-ID.
WebSockets leave HTTP entirely: a handshake upgrades the connection (101 Switching Protocols, RFC 6455), after which both sides exchange text or binary frames with ~2–14 bytes of overhead, full-duplex, until someone closes the socket. Maximum capability — and everything above the frame (message format, acks, reconnection, resume) is yours to design.
SSE vs. WebSockets vs. long polling vs. short polling
| Short polling | Long polling | SSE | WebSockets | |
|---|---|---|---|---|
| Direction | Pull | Simulated push | Server → client | Full-duplex |
| Protocol | Plain HTTP | Plain HTTP | Plain HTTP stream | Own protocol after upgrade |
| Delivery latency | interval/2 + RTT | ~RTT | ~RTT | ~RTT |
| Payloads | Any | Any | UTF-8 text only | Text + binary |
| Auto-reconnect | Trivially (next poll) | Re-request loop | Built in + Last-Event-ID | You write it |
| Proxy/firewall friction | None | Low | Low (buffering caveats) | Upgrade can be stripped |
| Server state | None | Held requests | Open streams | Pinned sockets |
| Complexity | Trivial | Moderate | Low | Highest |
| Sweet spot | Rare changes | Legacy fallback | Feeds, notifications, AI streams | Chat, games, collaboration |
How much does polling cost? The overhead math
The comparison no ranking page runs — 1,000 clients on a 2-second poll versus the same 1,000 on push:
1,000 clients, 2 s polling 1,000 clients, push
→ 500 requests/second sustained → 0 requests at rest
→ ~800 B of headers per cycle → SSE event framing ~5 B
→ ~0.4 MB/s of pure header tax → WebSocket frame overhead 2–14 B
→ nearly every response empty → bytes flow only when data exists
→ avg. delivery delay: 1 s + RTT → delivery delay: ~RTT
The lesson cuts both ways. Push wins overwhelmingly when updates are frequent and latency matters. But if data changes hourly, those 500 req/s never materialize — a gentle poll (or refetch-on-focus) is cache-friendly, debuggable with curl, serverless-compatible, and free of connection state. Dismissing polling entirely is fashion, not engineering.
Reconnection: the quiet differentiator
Connections drop — cell handoffs, laptop lids, proxy idle timeouts (often 30–120 s, which is why SSE streams send comment keep-alives and WebSockets exchange ping/pong frames). What happens next separates the transports. SSE’s contract handles it: the browser retries with the server-set retry delay and presents Last-Event-ID, so a server that keeps a short event buffer resumes the stream gap-free. A raw WebSocket simply closes: exponential-backoff reconnection, missed-message recovery, and resume protocol are all application code — the single most underestimated line item in “we’ll just use WebSockets.” Either way, a client offline for minutes needs catch-up (re-running the base query), not just reconnection — push transports deliver deltas, and deltas assume a baseline.
Why AI chat streams over SSE
Token-by-token model output is the perfect SSE workload: strictly one-directional, text, bursty, over plain HTTP that every proxy and CDN understands, with resume semantics for dropped generations. That is why LLM APIs overwhelmingly stream completions as text/event-stream — and why the pattern is worth knowing beyond chatbots: progress feeds, build logs, and dashboards share the same shape. WebSockets enter AI products at the voice layer, where the user interrupts mid-stream — the moment the client needs to talk back, the duplex tax buys something.
Scaling push: why statefulness matters
Polling and SSE are ordinary HTTP to a load balancer; any server can answer any request (SSE holds a stream but keeps HTTP semantics, needing only unbuffered proxies). WebSockets are state: each socket pins a client to a process, so horizontal scale means connection-aware balancing, draining on deploys, and a pub/sub backplane (commonly Redis) so a message published on server A reaches sockets held by server B. On HTTP/1.1, SSE has its own famous footgun — six connections per origin shared across tabs (MDN’s warning) — retired by HTTP/2, where streams multiplex over one connection. None of this is exotic; all of it is why managed real-time layers exist.
Common use cases
- Notification feeds and tickers — one-way, text, frequent: SSE’s home turf.
- AI response streaming — the current killer SSE use case; one direction, token text, resume on drop.
- Chat and collaboration — messages flow both ways with low latency: WebSockets, usually via a managed layer.
- Live dashboards — server-push of query results; in practice a live query subscription rather than a hand-rolled transport.
- Slowly changing data — inventory that updates hourly, settings screens: polling or refetch-on-focus, honestly.
Which transport should you use? A decision matrix
| Your situation | Reach for |
|---|---|
| Server → client only (feeds, streams, progress) | SSE |
| Client and server both send in real time | WebSockets |
| Updates rarer than every few minutes | Short polling / refetch on focus |
| Hostile proxies, legacy infrastructure | Long polling as fallback |
| Binary or high-frequency payloads | WebSockets |
| Serverless platform, function timeouts | Polling or SSE via streaming-capable hosts |
| ”I just want live data on screen” | A live-query layer that owns the transport |
Limitations and trade-offs
- SSE is text-only and one-way. Binary needs encoding; any client-to-server chatter rides separate HTTP requests — fine for acks, wrong for chat.
- WebSockets put you in the protocol business. Framing, acks, reconnect, resume, backpressure: the transport is easy, the contract on top is the work.
- Polling’s simplicity hides a latency floor. No tuning escapes interval/2 average delay; shrinking the interval just buys the header tax back.
- Long polling is the worst of both at scale. Held requests consume server capacity like push, while paying per-message reconnect overhead like pull — which is why it survives only as a fallback rung.
- All push transports need proxy cooperation. Stream buffering silently breaks SSE; stripped upgrade headers break WebSockets — test through the real infrastructure, not localhost.
Real-time transports on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The transport decision is largely absorbed by Live Queries: the code tabs above subscribe to a query and receive pushed events over a WebSocket fleet the platform operates — connections, reconnects, permission checks, and cross-server fan-out included — so “SSE or WebSockets?” becomes an implementation detail you inherit rather than infrastructure you build. Where a gentler cadence genuinely fits, the same query runs as a plain fetch on your schedule; polling a Parse query and subscribing to it are one line apart, which makes the right transport per feature a refactor, not a rewrite.
Frequently asked questions
Which is better, SSE or WebSockets?
Neither universally — the question is direction. SSE is the simpler tool when data flows one way, server to client: plain HTTP, automatic reconnection, works through ordinary proxies. WebSockets earn their extra complexity when the client must also send in real time — chat, games, collaborative editing.
Is SSE faster than polling?
For delivery latency, decisively: events arrive when they happen, while polling averages half the poll interval plus a round trip. It is also cheaper — one held connection instead of a stream of mostly-empty request/response cycles, each paying full HTTP header overhead.
When should I use long polling?
As a fallback, not a first choice: it exists for environments where persistent connections fail — legacy intermediaries, proxies that strip WebSocket upgrades or buffer streams. The server holds each request open until data arrives, which approximates push at the cost of reconnect churn and per-request header overhead.
How many SSE connections can a browser open?
Over HTTP/1.1, six per origin — and the limit is shared across tabs, a documented footgun where a dashboard open in seven tabs silently starves. Over HTTP/2 the constraint moves to concurrent streams multiplexed on one connection (default around one hundred), which effectively retires the problem.
Does SSE reconnect automatically?
Yes — it is the feature that most distinguishes it from raw WebSockets. EventSource retries dropped connections on its own, honors a server-set retry interval, and sends the last received event ID in a Last-Event-ID header so the server can resume the stream without gaps. WebSocket reconnection is code you write.
Can SSE send binary data?
No — the stream is UTF-8 text by specification; binary payloads must be encoded, at roughly a third of size overhead. WebSockets carry binary frames natively, which matters for audio, protocol buffers, and anything already compact.
Do WebSockets scale?
Yes, with architecture: each connection is state pinned to a server process, so horizontal scaling needs connection-aware load balancing and a pub/sub backplane to route messages across servers. SSE and polling ride ordinary HTTP semantics, which keeps load balancers and serverless platforms happier.
What do AI chat apps use to stream responses?
Server-Sent Events, almost universally: token-by-token generation is one-directional streaming text, which is precisely SSE's shape — plain HTTP out, no upgrade negotiation, automatic resume. WebSockets appear in AI products only when the client must interrupt or speak mid-stream, as in voice interfaces.
What is the fallback order for real-time features?
Feature-detect and degrade: WebSocket where the path supports it, SSE where only server-push is needed or upgrades fail, long polling as the lowest common denominator. Mature real-time libraries negotiate this ladder automatically — one reason raw sockets are rarely used bare in production.