Server-Sent Events (SSE) vs WebSockets: What's the Real Difference?
Both push data from a server to a browser without polling — but SSE is one-way over plain HTTP, while WebSockets are full-duplex over their own protocol. Picking the wrong one means either fighting infrastructure or over-engineering a simple feed.
Server-Sent Events (SSE) and WebSockets both solve the same basic problem — getting new data from a server to a browser the instant it's available, instead of the browser repeatedly asking "anything new?" — but they solve it in fundamentally different ways, with different trade-offs that make one clearly better than the other depending on what you're actually building.
What Server-Sent Events actually are
SSE is a one-way channel: the server streams a continuous sequence of text messages to the browser over a single, long-lived, ordinary HTTP connection. The browser opens it once, using the built-in `EventSource` API, and the server keeps that same HTTP response open indefinitely, writing a new chunk of data to it whenever something happens. Critically, SSE runs over plain HTTP — no special protocol, no separate handshake beyond a normal request, and no ability for the browser to send data back over that same connection. If the browser needs to send something to the server, it does so with an entirely separate, ordinary request (a regular `fetch` or form submission).
const stream = new EventSource("/api/notifications");
stream.onmessage = (event) => {
console.log("New message:", event.data);
};
stream.onerror = () => {
// EventSource retries the connection automatically — no manual reconnect logic needed.
};Sponsored
What WebSockets actually are
A WebSocket is a full-duplex connection — both the server and the browser can send messages to each other at any time, independently, over the same open connection. It starts as a regular HTTP request that includes an `Upgrade: websocket` header; if the server agrees, that connection is upgraded from HTTP into the separate WebSocket protocol (`ws://` or `wss://` for the encrypted version), and from that point on it's no longer HTTP at all — it's a persistent, bidirectional pipe that either side can write to whenever it wants.
const socket = new WebSocket("wss://example.com/chat");
socket.onmessage = (event) => console.log("Received:", event.data);
socket.onopen = () => socket.send("Hello from the client");The core difference: directionality
This is the single most important distinction, and it's what should actually drive the choice between them. SSE is one-way, server-to-client only. WebSockets are two-way, either side can initiate at any time. If your feature genuinely only needs the server to push updates — a live notification feed, a stock ticker, a progress indicator, streaming tokens from an AI response — SSE does exactly that with meaningfully less complexity. If your feature needs the client to send frequent, ad-hoc messages the server should react to immediately as part of the same real-time channel — a chat app, a multiplayer game, a collaborative document editor — you genuinely need the two-way channel WebSockets provide.
Protocol and data-format differences
- Transport: SSE is plain HTTP/HTTPS — the same protocol, ports, and infrastructure as every other request on your site. WebSockets use their own protocol (`ws`/`wss`) layered on top of an initial HTTP upgrade handshake.
- Data format: SSE messages are always UTF-8 text, formatted as simple `data: ...` lines with a blank line separating messages — no binary support. WebSockets support both text and binary frames natively, which matters for things like transmitting images, audio chunks, or compact binary protocols.
- Message structure: SSE has a lightweight built-in event format (an optional `event:` type, an `id:` for resuming, and the `data:` payload). WebSockets deliver raw messages with no built-in structure — you define your own message format entirely (commonly JSON) on top of it.
Built-in reconnection: a genuinely underrated SSE advantage
`EventSource` automatically reconnects if the connection drops, with no code required from you — the browser handles retry timing itself, and if the server included a `Last-Event-ID`, a well-built server can resume the stream from where it left off rather than starting over. WebSockets provide none of this: if a WebSocket connection drops (a network blip, a server restart, a proxy timeout), your application code is entirely responsible for detecting the disconnect and manually reconnecting, typically with your own retry/backoff logic. For a feed where occasionally missing a moment of connectivity is fine as long as it self-heals, this built-in behavior is a real, practical reason SSE ends up being less code to get right in production, not just a smaller feature set.
How they behave through proxies, load balancers and firewalls
Because SSE is ordinary HTTP, it generally passes through existing corporate proxies, CDNs, and load balancers without any special configuration — the infrastructure already knows how to handle long-lived HTTP responses. WebSockets, being a different protocol running on an upgraded connection, sometimes need explicit support at every layer in between: some older proxies and restrictive corporate firewalls block the WebSocket upgrade entirely, and load balancers need to be configured for sticky sessions or a compatible routing mode so that both directions of a given WebSocket connection consistently reach the same backend server. This is a real, practical factor when picking a technology for an app whose users might sit behind unpredictable corporate networks.
The HTTP/1.1 connection-limit gotcha
A specific, easy-to-miss trap: browsers historically cap the number of simultaneous HTTP/1.1 connections to a single domain at around 6. Since each SSE connection holds one of those slots open indefinitely, opening multiple SSE streams to the same domain from multiple browser tabs (or multiple SSE connections within one page) can silently exhaust that limit and start blocking other requests to the same domain. HTTP/2 removes this problem by multiplexing many streams over a single underlying connection, so if SSE is a core part of your app, serving it over HTTP/2 avoids this ceiling entirely. WebSockets don't share this specific limitation, since each one negotiates its own dedicated connection outside the normal HTTP request-connection pool.
What a minimal SSE server endpoint actually looks like
The server side is simpler than it sounds — it's just an HTTP response that never closes, with the right content type and a specific text format:
app.get("/api/notifications", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
const interval = setInterval(() => send({ time: Date.now() }), 5000);
req.on("close", () => clearInterval(interval));
});That's the entire server-side contract: set the `text/event-stream` content type, keep the connection open, and write `data: ...\n\n` blocks whenever there's something new to send. Compare that to a WebSocket server, which needs an actual WebSocket library (like `ws` or Socket.IO) to handle the protocol upgrade handshake, framing, and connection lifecycle — SSE's simplicity on the server is a real, practical advantage for a purely one-directional feed.
A security detail that catches people off guard: EventSource can't set custom headers
The browser's built-in `EventSource` API has a real limitation worth knowing before you rely on it: it can't send custom HTTP headers, which means the common pattern of authenticating a request with an `Authorization: Bearer <token>` header simply isn't available to it. In practice, SSE endpoints authenticate a different way — usually via an existing session cookie (which the browser sends automatically), or by passing a short-lived token as a query parameter (`/api/notifications?token=...`), accepting the minor security trade-off of a token appearing in server logs and browser history unless it's short-lived and scoped narrowly. WebSockets have the same header limitation during the initial handshake in some contexts, but libraries built on top of the raw WebSocket API often provide more flexible ways to pass auth data during the connection setup. If your app already authenticates via cookies, this is a non-issue; if it's a pure token-based API, it's worth designing for upfront rather than discovering it after building the feature.
Debugging SSE and WebSocket connections in the browser
Both are inspectable in the browser's DevTools Network tab, but they show up differently. An SSE connection appears as a single, ongoing request (type `eventsource`) — clicking it shows an "EventStream" tab listing every message received, in order, with timestamps, which makes debugging a stalled or malformed feed straightforward. A WebSocket connection appears as its own entry with a "Messages" tab showing every frame sent and received in both directions, color-coded by direction — genuinely useful for spotting whether a message never left the client, never arrived at the server, or arrived but wasn't handled correctly on the way back.
When SSE is genuinely the better choice
- Streaming AI-generated responses token by token — exactly the pattern behind ChatGPT-style "typing" effects, where the server only ever needs to push, never receive, over that channel.
- Live notifications or activity feeds — new comment alerts, order status updates, a dashboard that refreshes when new data lands.
- Log tailing or build/deployment progress — streaming console output as it's produced, a genuinely one-directional flow.
- Stock tickers, sports scores, or any "numbers that update live" display where the client never talks back over that channel.
When WebSockets are genuinely necessary
- Chat applications, where both sides send messages interchangeably and low latency in both directions matters.
- Multiplayer games, where player actions need to reach the server and other players' updates need to come back, continuously, in both directions.
- Collaborative editing (like multiple people typing in the same document simultaneously), where every keystroke potentially needs to travel in both directions with minimal delay.
- Any protocol requiring binary data over the real-time channel itself, since SSE is text-only.
Can you combine the two approaches instead of choosing?
Yes, and it's a legitimate, common pattern: use SSE for the server-to-client push, and a completely separate, ordinary HTTP request (a normal `fetch` POST) whenever the client needs to send something. This gets you most of the practical benefit of a real-time feed with the simplicity, infrastructure-friendliness, and automatic reconnection of SSE, while still letting the client communicate — it just does so over a different, stateless request rather than the same persistent channel. This pattern works well specifically when client-to-server messages are infrequent (submitting a form, sending an occasional command) rather than a continuous, low-latency back-and-forth, which is where a true WebSocket earns its extra complexity.
Browser support and fallbacks
Both `EventSource` and `WebSocket` are supported in every modern browser, so compatibility itself is rarely the deciding factor today. The more relevant support question is environment-specific: some older or very locked-down corporate proxy setups, and certain mobile carrier networks, have historically interfered with long-lived WebSocket connections more than they interfere with an ordinary long-lived HTTP response — which is one more practical point in SSE's favor for a public-facing feature whose users' network conditions you don't control, on top of its structural simplicity. Server-side, both are well supported across Node.js, Python, and virtually every modern backend framework, so the choice rarely comes down to what your stack can technically support.
SSE and WebSockets vs plain polling
It's worth placing both against the simpler alternative they're usually chosen over: polling, where the client just re-requests the same endpoint every few seconds regardless of whether anything changed. Polling is the simplest to implement and works everywhere with zero special infrastructure, but it wastes requests when nothing has changed and introduces a delay up to your polling interval before new data is even fetched. Long polling — where the server holds the request open until there's actually something new to return, then the client immediately re-requests — narrows that delay without needing SSE or WebSockets, but still involves repeatedly re-establishing connections and doesn't scale as cleanly as either real-time option under high message frequency. SSE and WebSockets both remove the polling delay and the wasted no-op requests entirely, at the cost of holding a connection open continuously.
Common mistakes with each
- Choosing WebSockets by default for a purely one-directional feed — adding write-side complexity (manual reconnection, connection-state management) that SSE would have handled for free.
- Opening many separate SSE connections to the same domain on HTTP/1.1, silently hitting the browser's per-domain connection cap and starving other requests.
- Forgetting that a WebSocket connection can silently die without a clean close event on some networks, and not implementing a heartbeat/ping-pong check to detect a truly dead connection versus one that's just quiet.
- Not configuring load balancer session affinity for WebSockets, causing intermittent connection failures when a reconnect lands on a different backend instance that doesn't have the original connection's state.
- Trying to send binary data over SSE, which isn't supported — base64-encoding it into the text stream works but adds overhead; a WebSocket is the more direct fit if binary payloads are a core requirement.
The short version: SSE is a simpler, one-way, plain-HTTP stream with automatic reconnection built in — the right default for anything that's purely server-to-client. WebSockets are a genuinely bidirectional, separate protocol that's necessary the moment the client needs to send frequent, low-latency messages over the same real-time channel. Most "live updates" features people reach for WebSockets to build are actually one-directional and would be simpler, and just as effective, as SSE.
Tools used in this article
Sponsored
Frequently asked questions
For pure server-to-client delivery, they're comparably fast — the meaningful difference isn't raw speed, it's directionality and how much code each requires you to write for reconnection and infrastructure compatibility.
TechToolsCenter Team
Product & Tools
The team behind TechToolsCenter — building fast, private, browser-based tools and writing practical guides on how to get the most out of them.
Related articles
What Is a CDN, and How Does It Actually Work?
A CDN doesn't make your server faster — it puts copies of your content physically closer to the people requesting it, so the distance data has to travel shrinks instead of the server itself changing.
SQL vs NoSQL Databases: What's Actually Different
The real divide isn't "SQL is old, NoSQL is new" — it's whether your data fits neatly into consistent, related tables, or is naturally varied, nested, and easier to reason about as flexible documents.
What Is Idempotency in APIs, and Why Does It Matter?
An idempotent request can be safely retried as many times as a flaky network demands, without the risk of double-charging a card or double-creating an order — here's what actually makes a request idempotent.