SSE vs WebSockets in 2026: Which One Should You Use?

Profile picture of Arvucore Team

Arvucore Team

September 22, 2025 · Updated August 26, 2026

14 min read

If you only need to push data from the server to the browser, use Server-Sent Events: they run over plain HTTP, reconnect on their own, pass through proxies and serverless platforms, and handle notifications, dashboards, and LLM token streaming well. Use WebSockets when the client must also send many small messages with low latency, or when you need binary frames, as in chat, collaborative editing, and games. Polling and long polling remain fallbacks, not first choices.

How SSE and WebSockets work at the protocol level

Server-Sent Events are ordinary HTTP. The browser sends a GET, the server answers with Content-Type: text/event-stream and never closes the response. Events are UTF-8 text lines: data:, optional event: and id:, separated by a blank line. Anything that can stream an HTTP response can serve SSE.

On HTTP/1.1 that has one well-known cost: browsers cap connections per origin at six. Each open EventSource holds one of them, so a page with several streams, or a user with several tabs, can exhaust the budget and stall normal requests. Over HTTP/2 the problem disappears: every SSE stream is a multiplexed stream inside a single TCP connection, and the per-origin limit is on the order of a hundred streams, negotiated with the server. In 2026 the limit mostly bites in local development and behind legacy proxies that downgrade to HTTP/1.1.

WebSockets start as HTTP and stop being HTTP. The client sends a GET with Upgrade: websocket and Connection: Upgrade; the server answers 101 Switching Protocols, and from then on the TCP socket carries WebSocket frames (RFC 6455): text or binary, with opcodes, fragmentation, masking from client to server, and ping/pong control frames. Over HTTP/2 there is a separate mechanism (RFC 8441, the extended CONNECT method) that tunnels a WebSocket inside one HTTP/2 stream, but support across servers, proxies, and libraries is inconsistent, so most deployments still run WebSockets on a dedicated HTTP/1.1 connection.

Everything downstream reacts to this difference: an SSE stream is a long HTTP response; a WebSocket is an opaque socket that HTTP tooling no longer understands.

Reconnection, ordering, and Last-Event-ID

This is where SSE earns its reputation for simplicity. EventSource reconnects automatically when the connection drops, using the retry: interval the server may set. If the server tags events with id:, the browser sends the last one back as a Last-Event-ID header on reconnect. The server replays what the client missed from a log, a Redis stream, or a Kafka offset. Resume is part of the spec.

WebSockets have none of this. When the socket closes, the WebSocket object is dead. Your code must detect the close, back off with jitter so thousands of clients do not reconnect at the same moment, re-authenticate, resubscribe, and reconcile state. If you need "give me what I missed," you design it yourself with sequence numbers or a resumable session ID.

Two notes for both transports. Idle connections die: carriers, NAT tables, and load balancers drop sockets quiet for tens of seconds to a few minutes, so send a heartbeat (an SSE comment : ping or a WebSocket ping frame) inside that window. And EventSource retries forever by design, so a permanent error such as a 401 must be handled explicitly.

Proxies, load balancers, and CDNs

SSE looks like a slow HTTP response. Every L7 proxy, ingress, and CDN forwards it, but several buffer responses by default, which turns a stream into nothing until the buffer fills. Disable buffering per route (X-Accel-Buffering: no for Nginx, the equivalent in your ingress or CDN), disable compression on the stream or use a compressor that flushes, and raise idle and read timeouts for that path. Most "SSE does not work behind our proxy" incidents are one of these three settings.

WebSockets need the proxy to honor the Upgrade handshake and pass the socket through. Modern proxies, cloud load balancers, and CDNs do, often with conditions: specific plans, idle timeouts you cannot raise, or no support in an older corporate middlebox. Restrictive networks can still block wss://, which is why long-polling fallbacks exist. Some CDNs meter WebSocket connections differently from HTTP requests. L4 passthrough avoids most of this but gives up L7 routing and observability.

HTTP/3 and QUIC change little here for now: SSE works over HTTP/3 like any response, while WebSocket over HTTP/3 (RFC 9220) is still rarely deployed end to end.

Authentication: cookies vs headers

Neither browser API lets you set an Authorization header. This surprises teams with a clean bearer-token setup for their REST API who then discover the real-time channel cannot reuse it.

Your options:

  • Cookies. Both EventSource (with withCredentials: true for cross-origin) and WebSocket send cookies. This is the simplest path when the front end is same-site. Set SameSite, Secure, and HttpOnly, and validate the Origin header on the server, because WebSockets are not covered by CORS.
  • Token in the query string. Works for both, but the token lands in access logs, proxy logs, and browser history. Use a short-lived, single-purpose ticket issued by an authenticated POST, not your long-lived session token.
  • Token in the first message (WebSocket only). Open the socket, send { "type": "auth", "token": "..." }, and close if it does not arrive within a second. Standard in practice; still means the connection exists briefly unauthenticated.
  • Replace EventSource with fetch. A streamed fetch with response.body.getReader() accepts any header, works with AbortController, and can parse text/event-stream with a few lines of code or a small library. You lose automatic reconnection and Last-Event-ID, which you re-implement. This is the pattern most LLM chat interfaces use today.

The Sec-WebSocket-Protocol header is sometimes abused to carry a token; it works, but it ties auth to a field meant for subprotocol negotiation. For tokens and sessions in general see modern authentication with OAuth 2.0, JWT, and zero trust.

Scaling and fan-out: pub/sub, Redis, sticky sessions

A single node can hold tens to hundreds of thousands of idle connections, depending on runtime, memory per connection, and TLS. The problem is never the count; it is that a message produced on node A must reach a client connected to node B.

The standard answer is the same for both transports: keep the connection nodes stateless, and put a pub/sub fabric behind them. Redis Pub/Sub or Redis Streams, NATS, or Kafka carry events; every node subscribes to the channels its clients care about and writes to the local sockets. Redis Streams and Kafka also give you the replay log that Last-Event-ID needs. Design the topic model early; unbounded subscriptions are the usual cause of memory growth.

Where the transports differ:

  • SSE balances like any HTTP request. Any node can serve any client, and a reconnect can land anywhere, because resume state travels in Last-Event-ID. Sticky sessions are optional.
  • WebSockets are stateful by nature. If the server keeps per-connection state (subscriptions, presence, cursors) in memory, a reconnect must return to the same node, which means sticky sessions, or that state must be externalized. Managed gateways and frameworks with a Redis backplane exist mostly to solve this.
  • Backpressure is a WebSocket concern on both directions and an SSE concern on one. Bound per-connection queues, coalesce updates for slow clients, and drop or downgrade instead of letting bufferedAmount grow.

Deploys are a scaling event in disguise: a rollout closes every connection on the node, and tens of thousands of clients reconnect at once. Drain gracefully, spread restarts, and make sure client backoff has jitter.

Serverless, browsers, and mobile

Serverless. Functions are request-scoped and time-limited. WebSockets cannot live inside a normal function; you need a managed gateway that holds the socket and invokes functions per message, plus a store for connection IDs. SSE fits better: a streaming or edge function returns a ReadableStream and keeps it open up to the platform limit, typically minutes, with the client reconnecting afterward. Actor-style primitives such as Durable Objects hold WebSockets across requests where you need them. See serverless computing for the cost model that makes long connections expensive on per-invocation billing.

Browsers. Both APIs are supported in every current browser, desktop and mobile. EventSource is text-only and GET-only; WebSocket supports binary and has no such restrictions.

Mobile apps. Native platforms suspend background apps and close their sockets. No transport survives this; anything that must reach a backgrounded or offline user goes through APNs, FCM, or Web Push, with the live channel used only in the foreground. WebSocket libraries are mature on iOS and Android; SSE clients are thinner, and many teams use a streaming HTTP request. Battery cost is driven by radio wake-ups, so a 20-second heartbeat hurts more than the protocol choice.

WebSocket cons: the drawbacks explicitly

WebSockets are powerful and often the wrong default. The specific costs:

  1. No built-in reconnection or resume. Every team re-implements backoff, resubscription, and missed-message recovery.
  2. No custom headers from the browser. Auth is cookies, query strings, or a first-message handshake.
  3. Stateful connections. Sticky sessions or an external state store; harder blue-green and rolling deploys.
  4. Poor serverless fit. Needs a dedicated gateway and connection registry; billing by connection-minute adds up.
  5. Outside the HTTP toolchain. No HTTP caching, no standard compression unless permessage-deflate is negotiated, no request logging per message, and observability requires custom instrumentation.
  6. Proxy and network friction. Upgrade must be honored end to end; some corporate networks and CDN plans block or limit it.
  7. Not CORS-protected. Cross-site WebSocket hijacking is real if you rely on cookies without an Origin check.
  8. Custom protocol on top. Message framing, versioning, and error semantics are yours to define and to keep backward-compatible.
  9. Head-of-line blocking on one TCP connection. A large frame delays everything behind it; there is no stream prioritization.
  10. Higher operational surface. Connection limits, file descriptors, TLS CPU, and load tests need protocol-aware tooling.

None of these is disqualifying for chat or collaboration. They are the bill for bidirectionality; pay it only when you use it.

Polling and long polling: the fallbacks

Short polling issues a request on a timer. It is universally compatible, cacheable, and the right answer for data that changes every few minutes. At one request per second per client it becomes the most expensive option of all, with latency bounded by the interval.

Long polling holds the request open until data arrives or a timeout fires, then the client re-requests. It approximates push over plain HTTP and works behind almost anything. The cost is a full HTTP round trip per message, a gap between responses where events are missed unless you carry a cursor, and the same HTTP/1.1 connection limits as SSE, without the spec-defined reconnect.

In 2026 long polling is a fallback for networks that break both SSE and WebSockets, not a primary design. If your library still defaults to it, check why.

Comparison table: SSE vs WebSockets vs long polling

Criterion Server-Sent Events WebSockets Long polling
Direction Server to client (client uses normal requests) Full duplex Client-initiated, server holds
Transport HTTP/1.1, HTTP/2, HTTP/3 response stream Upgraded TCP socket; HTTP/2 tunnel rarely deployed Plain HTTP requests
Reconnection Automatic, built into EventSource Manual Manual (each request)
Resume after drop Last-Event-ID, spec-defined Custom sequence numbers Custom cursor
Binary support No (text, base64 if needed) Yes, native frames Yes (response body)
Auth in browser Cookies, query ticket, or fetch with headers Cookies, query ticket, first message Any header
Proxy and CDN friendliness High; disable buffering and raise timeouts Medium; needs Upgrade support end to end Highest
Per-origin limit on HTTP/1.1 6 connections; solved by HTTP/2 Separate connection each 6 connections
Scaling model Stateless nodes plus pub/sub, no stickiness needed Pub/sub plus sticky sessions or external state Stateless
Serverless fit Good on streaming or edge functions Requires a managed gateway Good
Message overhead Low (a few bytes per event) Lowest (2 to 14 bytes framing) High (full HTTP per message)
Best use cases Notifications, dashboards, feeds, LLM streaming Chat, collaborative editing, games, trading Fallback, low-frequency updates

Code: EventSource client, Node SSE endpoint, WebSocket echo

A minimal SSE endpoint in Node without frameworks. Note the headers that stop proxies from buffering and the id: that enables resume.

// server.js — Node 20+, no dependencies
import http from "node:http";

http.createServer((req, res) => {
  if (req.url !== "/events") { res.writeHead(404).end(); return; }

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache, no-transform",
    "Connection": "keep-alive",
    "X-Accel-Buffering": "no",
  });

  let id = Number(req.headers["last-event-id"] ?? 0);
  const timer = setInterval(() => {
    id += 1;
    res.write(`id: ${id}\nevent: tick\ndata: ${JSON.stringify({ t: Date.now() })}\n\n`);
  }, 1000);
  const heartbeat = setInterval(() => res.write(": ping\n\n"), 15000);

  req.on("close", () => { clearInterval(timer); clearInterval(heartbeat); });
}).listen(3000);

The browser side is a few lines, and reconnection is free.

const es = new EventSource("/events", { withCredentials: true });
es.addEventListener("tick", (e) => console.log(JSON.parse(e.data)));
es.onerror = () => console.warn("disconnected, browser will retry");

A minimal WebSocket echo with the ws package. Compare what the client has to do when the socket drops.

// ws-server.js
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 3001 });
wss.on("connection", (socket, req) => {
  if (req.headers.origin !== "https://app.example.com") { socket.close(1008); return; }
  socket.on("message", (data) => socket.send(data));
});
// client
let ws, attempt = 0;
function connect() {
  ws = new WebSocket("wss://api.example.com/ws");
  ws.onopen = () => { attempt = 0; ws.send("hello"); };
  ws.onmessage = (e) => console.log(e.data);
  ws.onclose = () => setTimeout(connect, Math.min(30000, 500 * 2 ** attempt++) * (0.5 + Math.random()));
}
connect();

Both servers should sit behind TLS in production; neither example handles authentication beyond an Origin check.

Decision checklist: when to use which

  • Chat (1:1, group, support). WebSockets. Messages flow both ways constantly, typing indicators and presence are cheap on an open socket. SSE plus POST works for low-volume support widgets.
  • Live dashboards and monitoring. SSE. One-way, text, tolerates a second of reconnect. Coalesce updates server-side; see dashboard development for the data side.
  • Notifications and activity feeds. SSE while the tab is open; Web Push, APNs, or FCM when it is not. Never a WebSocket just for notifications.
  • Collaborative editing. WebSockets. CRDT or OT sync needs frequent small bidirectional messages and often binary encodings.
  • Multiplayer games. WebSockets today; WebTransport where you can require HTTP/3 and want unreliable datagrams for position updates.
  • LLM token streaming. SSE, or a streamed fetch parsing text/event-stream when you need headers and cancellation. This is what the major model APIs expose, and it composes with serverless.
  • Trading and market data. WebSockets for order flow; SSE is acceptable for read-only tickers. Fan-out via Redis or Kafka either way.
  • IoT telemetry to browser. MQTT over WebSockets from devices, pub/sub in the middle, SSE or WebSockets to the dashboard depending on whether operators send commands.
  • Voice or video. Neither. Use WebRTC, with WebSockets or SSE only for signaling.
  • Restrictive corporate networks. SSE first, long polling as the last resort.

WebTransport: the emerging option

WebTransport is the browser API over HTTP/3 and QUIC: multiple independent streams with no head-of-line blocking, and unreliable datagrams for data where late is worse than lost. For games, media, and high-frequency telemetry it removes the two structural limits of WebSockets: one TCP stream and reliable-only delivery.

Adoption is the issue. Browser support is broad on Chromium and improving elsewhere, but servers, load balancers, and CDNs lag, and UDP-hostile networks need a WebSocket path anyway. In 2026 it is justified for a narrow set of products and a research item for everyone else. Abstract the transport so it can change; do not plan the migration yet.

Recommendation

Start with SSE for anything server-to-client, paired with normal HTTP requests in the other direction. Serve it over HTTP/2, disable proxy buffering, set id: on every event, and back it with a replayable log so Last-Event-ID recovers missed data. Move a feature to WebSockets only when it needs high-frequency bidirectional traffic or binary frames, and budget for reconnection logic, an auth handshake, a pub/sub layer, and sticky sessions or externalized state. Keep long polling as a fallback behind a flag, not as a design. At Arvucore we usually recommend one transport per feature, chosen by the checklist above, rather than one for the whole product; the systems that age well are the ones where the pub/sub layer, not the socket, is the architecture.

Ready to Transform Your Business?

Let's discuss how our solutions can help you achieve your goals. Get in touch with our experts today.

Talk to an Expert

Tags:

websockets vs. ssereal-time communicationpush notificationsserver-sent eventswebsocket conslong polling
Arvucore Team

Arvucore Team

Arvucore’s editorial team is formed by experienced professionals in software development. We are dedicated to producing and maintaining high-quality content that reflects industry best practices and reliable insights.

Frequently asked questions

Is SSE better than WebSockets?
Neither is better in general. SSE is simpler and fits one-way server-to-client streams such as notifications, dashboards, and LLM token streaming. WebSockets are the right choice when the client also needs to send frequent messages with low latency, or when you need binary frames.
What are the main cons of WebSockets?
No automatic reconnection or resume, no custom headers on the browser handshake, stateful connections that need sticky sessions or a pub/sub layer, poor fit with serverless and some proxies or CDNs, and a separate protocol that bypasses standard HTTP tooling, caching, and compression.
Does SSE work over HTTP/2?
Yes. Over HTTP/2 each SSE stream is a multiplexed stream on one TCP connection, which removes the six-connections-per-origin limit that hurts SSE on HTTP/1.1. Most browsers and edge providers negotiate HTTP/2 by default with TLS.
Can I send custom headers with EventSource or WebSocket in the browser?
No. Neither browser API lets you set an Authorization header. Use cookies, a short-lived token in the query string, or replace EventSource with fetch and a streamed ReadableStream, which does accept headers.
Is long polling still relevant in 2026?
Only as a fallback for restrictive networks or very low-frequency updates. It works everywhere, but it wastes requests, adds latency, and is harder to reason about than SSE, which is supported by every modern browser.
Will WebTransport replace WebSockets?
Not yet. WebTransport runs over HTTP/3 and QUIC and offers unreliable datagrams and multiple streams without head-of-line blocking, but browser and infrastructure support is still uneven. Treat it as an option for games and media, not as a default.

Related articles

WebRTC Development in 2026: Architecture, Cost and Scope

WebRTC Development in 2026: Architecture, Cost and Scope

What WebRTC handles and what you must build yourself: P2P vs SFU vs MCU, TURN, managed platforms vs self-hosted, security, and what drives project cost.

Accessibility (A11y) in Web Development: WCAG 2.1 Guidelines

Accessibility (A11y) in Web Development: WCAG 2.1 Guidelines

As an Arvucore guide, this article explains Accessibility (A11y) in web development and WCAG 2.1 guidelines, offering practical advice for European decision makers and technical teams. It highlights how web accessibility development improves user experience, legal compliance, and market reach, including design considerations that integrate accessibility early in product lifecycles.

Agile Development: Scrum vs Kanban vs SAFe for Enterprise Projects

Agile Development: Scrum vs Kanban vs SAFe for Enterprise Projects

At Arvucore, we help enterprise teams choose agile frameworks that scale. This article compares Scrum, Kanban and SAFe to guide decision-makers evaluating agile company methodologies for large projects. We examine strengths, governance, metrics, tooling and suitability for complex portfolios, and how to integrate scrum kanban safe approaches with modern project management software to improve delivery and predictability.

API Gateway: Enterprise API Management and Security

API Gateway: Enterprise API Management and Security

An API gateway is central to enterprise API management and security, providing routing, policy enforcement, and observability for distributed systems. This article from Arvucore explores practical api gateway implementation approaches, governance and lifecycle strategies, and robust microservices security patterns. It helps business and technical leaders evaluate trade-offs, reduce integration risk, and accelerate secure API adoption across complex IT landscapes.