---
term: 'Server-Sent Events vs. WebSockets vs. Polling'
seoTitle: 'SSE vs. WebSockets vs. Polling: Choosing a Real-Time Transport'
headline: 'Server-Sent Events vs. WebSockets vs. Polling: which should you use?'
slug: sse-vs-websockets-vs-polling
category: api-realtime
shortDefinition: 'Polling is a pull model where clients ask repeatedly; SSE and WebSockets hold one connection open so the server can push in real time.'
relatedTerms:
  - websockets-real-time-sync
  - real-time-live-queries
  - pub-sub-pattern
  - api-payload-optimization
contrastsWith:
  - websockets-real-time-sync
aboutTerms:
  - 'Server-Sent Events (SSE)'
  - 'WebSockets'
  - 'Long Polling'
  - 'Short Polling'
faq:
  - question: 'Which is better, SSE or WebSockets?'
    answer: '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.'
  - question: 'Is SSE faster than polling?'
    answer: '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.'
  - question: 'When should I use long polling?'
    answer: '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.'
  - question: 'How many SSE connections can a browser open?'
    answer: '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.'
  - question: 'Does SSE reconnect automatically?'
    answer: '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.'
  - question: 'Can SSE send binary data?'
    answer: '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.'
  - question: 'Do WebSockets scale?'
    answer: '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.'
  - question: 'What do AI chat apps use to stream responses?'
    answer: '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.'
  - question: 'What is the fallback order for real-time features?'
    answer: '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.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Server-sent events — WHATWG HTML Living Standard'
    url: 'https://html.spec.whatwg.org/multipage/server-sent-events.html'
  - name: 'RFC 6455 — The WebSocket Protocol'
    url: 'https://datatracker.ietf.org/doc/html/rfc6455'
  - name: 'Using server-sent events — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events'
  - name: 'RFC 6202 — Known Issues with Long Polling and HTTP Streaming'
    url: 'https://datatracker.ietf.org/doc/html/rfc6202'
cta:
  title: 'Skip the transport decision'
  text: 'Back4app Live Queries deliver real-time updates over a managed WebSocket fleet — subscribe to the query you already have and let the platform own connections, reconnects, and fan-out.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-07-24'
translationKey: sse-vs-websockets-vs-polling
---

**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

```js
// 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:**

```javascript
// 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
// 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
```

**Swift:**

```swift
// 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
}
```

**Kotlin:**

```kotlin
// 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](https://datatracker.ietf.org/doc/html/rfc6202).

**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](https://html.spec.whatwg.org/multipage/server-sent-events.html)). 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](https://datatracker.ietf.org/doc/html/rfc6455)), 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.

```mermaid
flowchart LR
  accTitle: Polling pull versus SSE and WebSocket push
  accDescr: With polling the client repeatedly requests updates from the server and most responses are empty. With Server-Sent Events the server pushes events to the client over one held HTTP stream. With WebSockets client and server exchange frames in both directions over one persistent socket.
  subgraph P["Polling — pull"]
    C1["Client"] -->|"ask every n s (mostly empty)"| S1["Server"]
  end
  subgraph E["SSE — push"]
    S2["Server"] -->|"one HTTP stream of events"| C2["Client<br/>(EventSource)"]
  end
  subgraph W["WebSocket — duplex"]
    C3["Client"] -->|"frames"| S3["Server"]
    S3 -->|"frames"| C3
  end
```

## 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:

```text
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](/glossary/pub-sub-pattern/) 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](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)) — 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](/glossary/websockets-real-time-sync/), usually via a managed layer.
- **Live dashboards** — server-push of query results; in practice a [live query](/glossary/real-time-live-queries/) 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](/glossary/real-time-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.
