Cover illustration for “WebSocket vs Server-Sent Events for Live Dashboard Data”

Essay Websocket API

WebSocket vs Server-Sent Events for Live Dashboard Data

SSE works better for dashboards that only need one-way data flow from server to client.

A ticket lands on a developer's desk that says "make this update in real time," and the hands move before the brain does: open a socket, write the handshake code, ship it. This piece argues that reflex is wrong more often than it's right, and the fix is a single structural question asked before any code gets written: does the dashboard need to send data back to the server at high frequency, or is it just receiving a stream of updates?

For over a decade, that reflex made sense. WebSocket was the only browser-native option for a persistent, low-latency connection, so it became the answer to every "how do I make this live" question, whether the job needed two-way traffic or not. Buried in the HTML5 spec the whole time was a second option built for exactly this job: Server-Sent Events. SSE shipped in most major browsers by 2011 (Internet Explorer never picked it up, and Edge didn't add support until 2020), and it came with automatic reconnection built in, free of charge. It should have taken off immediately. It didn't, because of a quirk in HTTP/1.1: browsers capped connections per origin at six, and every open SSE stream used one permanently. A dashboard with a couple of SSE streams open could quietly stall every other request to that domain. SSE was standing in the room the whole time, just not invited to the party.

That math has changed, and so has the default. Where engineers once reached for WebSocket unless something forced them off it, the common starting point now is SSE, with WebSocket reserved for cases that genuinely need two-way traffic. A big push came from an unlikely place: large language models. OpenAI, Anthropic, and Google Gemini all built their token-streaming APIs on SSE, and that decision pulled a wave of tooling, libraries, and framework support along with it, closing the implementation gap that used to make WebSocket the path of least resistance. The rest of this piece walks through why that shift happened, what it costs to run each protocol at real scale, and how to pick the right one for a dashboard without guessing.

How the two protocols differ in mechanism and direction

Start with the one fact that matters most: WebSocket talks in both directions at once, and SSE only talks from the server to the client. It's the structural fact that should decide which one a dashboard team picks, long before anyone benchmarks latency.

WebSocket begins life as an ordinary HTTP request, then upgrades into a direct, full-duplex channel. The client and server shake hands, and once that handshake finishes, HTTP is gone. What's left is a direct, full-duplex channel over TCP, where either side can send a message at any moment with no request waiting for a matching response. It supports both text and binary frames, which gives it real flexibility for payloads. But frames from different messages can't be mixed on one connection, so a large message in flight blocks everything stuck behind it. And that initial handshake, the HTTP Upgrade step, adds a full round trip before any real data moves, a cost that adds up fast in situations where connections open and close often. WebSocket also hands the developer a chore list: reconnecting after a drop, pinging back and forth to catch a connection that died silently, and making sure messages don't get delivered twice when a client reconnects. None of that comes free.

SSE works the plain way. The client sends a normal GET request. The server answers with a header, Content-Type: text/event-stream, and then just never closes the response. It keeps writing events into that open connection as they happen. The client's side of the connection goes quiet after the first request. If the client needs to send something back, it fires off a separate, ordinary HTTP call, and that's the full extent of the exchange. The wire format has four fields, and all four come from the WHATWG HTML standard itself, not some third-party library: data: carries the payload, id: keeps events in order, event: names the type of event, and retry: tells the browser how long to wait before reconnecting. The browser's built-in EventSource API handles reconnection automatically. No code required. Catching up on missed events takes a bit more work: the browser sends a Last-Event-ID header when it reconnects, but the server has to store past events and know how to replay them. SSE does have one real limitation: it's text only. Binary data has to be Base64-encoded first, which adds overhead.

One wrinkle shows up often enough in dashboard work to flag directly. EventSource doesn't support custom headers, so the usual token-based auth pattern doesn't work out of the box. Teams get around this three ways: cookie-based auth, which tends to be the cleanest option, a short-lived token passed as a query parameter, or a one-time token exchange that happens before the connection opens. It's a workaround, one more thing to design around if SSE is the pick.

What each protocol costs at infrastructure scale

Mechanism explains how each protocol behaves on one connection. Scale is where the real bill comes due; the two protocols hand that bill to very different parts of the stack.

WebSocket connections are stateful. Each one holds a TCP channel open on a specific server process, for as long as the client stays connected. That's fine with one server. It gets complicated fast once a deployment runs on multiple nodes, say a Kubernetes cluster with several pods behind a load balancer. If a message needs to reach a user whose WebSocket connection landed on pod A, but the event that triggers the message gets generated on pod B, pod B has no way to reach that connection without help. The standard fixes are sticky sessions, which pin a client to the same pod for the life of its connection, or a pub/sub broker like Redis sitting in the middle to fan messages out across every node. Either way, it's infrastructure a team has to build and maintain on top of the protocol itself.

SSE skips most of that problem, because under the hood it's still a regular HTTP response. Load balancers and CDNs already know how to handle long-lived HTTP responses, so there's no special configuration needed and no sticky sessions required. Horizontal scaling across multiple servers still needs some kind of shared broker to fan events out to the right clients, but the connection layer stays plain, boring, stateless HTTP the whole way through. Firewalls and corporate proxies tend to like SSE more too: plenty of them intercept or block the WebSocket Upgrade handshake outright, while SSE, since it never leaves standard HTTP, usually passes through without anyone touching a config file.

SSL-inspecting proxies and older HTTP/1.0 proxies that don't understand chunked transfer encoding can buffer an SSE stream without telling anyone, putting a real crack in that compatibility story. Events pile up and arrive in bursts instead of as they happen. The exact enterprise network SSE is supposed to glide through is sometimes the one place it quietly breaks, and nothing in the browser tells a developer that's happening. It looks like a slow server when it's actually a proxy doing something unhelpful three hops away.

Two real deployments make the scaling argument concrete. Shopify built its Black Friday/Cyber Monday Live Map on SSE and reported full uptime through the event, with data reaching clients within milliseconds of being available, a sharp jump from the 2021 system's minimum ten-second poll interval. Counting the full pipeline, including Flink processing upstream, data showed up on the Live Map within seconds of being created. Separately, LinkedIn documented scaling SSE to hundreds of thousands of persistent connections on a single machine, after tuning the underlying server and kernel settings. Both cases show that SSE's plain-HTTP model can carry real enterprise load, as long as the infrastructure around it is tuned for the job.

It's the structural fact that should decide which one a dashboard team picks, long before anyone benchmarks latency.

The rendering bottleneck that protocol choice makes worse

A failure mode neither protocol's documentation warns about has nothing to do with the network. It lives entirely in the browser, after the data has already arrived.

A browser can only paint so many frames per second, and every incoming update is a little bit of rendering work queued up for the main thread. If updates arrive faster than the browser can draw them, that queue backs up. The screen starts to lag, stutter, or freeze for a beat, and it looks exactly like a network problem: slow server, dropped packets, bad connection. It's a rendering problem, caused by data arriving faster than pixels can update. A dashboard pushing updates every hundred millisecond is a common way to back yourself into this, especially when the protocol underneath makes sending that fast feel cheap.

Because sending a message over an open WebSocket connection is fast and lightweight, it's tempting to push updates as often as the data changes, with no real check on cadence. SSE's slightly heavier per-event cost under HTTP, combined with the convention of treating a stream as a feed rather than a firehose, tends to nudge developers toward more conservative, sane update intervals by default. Neither protocol enforces good judgment here. Decide how often a dashboard actually needs to refresh before picking a protocol, not after. If the honest answer to "how fast should this update" is "as fast as possible," that's a product decision that needs a second look, not a technical requirement to engineer around.

The structural question that determines which protocol fits a given dashboard

Diagram: One Question Decides the Protocol. Visualizes: Visualize a single binary decision that splits into two clear paths: 'Does the client need to send data back to the server at high, continuous frequency through the same real-time channel?' A…

Pick WebSocket when the client genuinely needs to send data back to the server at high, continuous frequency through that same real-time channel. Pick SSE when the dashboard is mainly consuming a stream of updates pushed from the server, with any client actions handled as separate, ordinary HTTP calls.

Genuine bidirectionality is rarer than it looks, and this is where plenty of engineers will recognize a call they made that didn't need to go the way it did. Occasional REST calls sitting next to a live update stream do not count as bidirectional traffic. That's two separate jobs, not one continuous two-way exchange, and treating them as the same thing is how a dashboard ends up with WebSocket infrastructure it didn't need. Real bidirectional cases look like collaborative editing, where several people are writing into the same document at once, or a financial trading dashboard, where a client's order actions have to travel the same low-latency path as the incoming price feed.

Everything else, which is most operational dashboards, fits the SSE side of the line. Monitoring panels, CI/CD status boards, deployment pipelines, analytics feeds, order tracking, system health displays: all of these are a server pushing a stream of updates to a screen, with any user action (filter this, acknowledge that, subscribe to a different channel) handled as its own small HTTP request. SSE's named event: field adds something useful here too: a single connection can carry several logical channels at once, so one monitoring dashboard can receive CPU, memory, and request-rate events all on the same stream instead of opening three separate connections for three separate metrics. One documented case: a Node.js deployment dashboard streaming live build status for several services (CI/CD triggers, test runs, deployments) ran on a single SSE endpoint per client, checking permissions once at connection time, with no WebSocket infrastructure anywhere in the stack.

One fallback belongs in the picture without getting equal billing: long polling. When neither WebSocket nor SSE can be relied on, inside an aggressive corporate proxy environment, or when supporting very old browsers, long polling still works, since it rides on plain request/response HTTP cycles that basically any infrastructure understands. It costs more overhead from all that repeated back-and-forth, and it should be treated as a last resort, not a real third option sitting next to WebSocket and SSE.

How Phantom Farm approaches real-time dashboard data delivery

Phantom Farm is built around exactly the server-push model this framework points to as the right fit for most live dashboards: SSE as the default way data moves, with WebSocket available for the cases that genuinely need two-way traffic. That's the structural line this piece has been drawing the whole way through, and Phantom Farm's setup sits right on it instead of forcing every project down one path.

The infrastructure advantages covered earlier (no sticky sessions, CDN compatibility, automatic reconnection handled by the browser) are the same properties built into what Phantom Farm offers teams out of the box. A team doesn't have to rediscover, the hard way, that SSE needs a message broker for fan-out at scale or that WebSocket needs sticky sessions without one. Those questions get answered by the platform before a line of dashboard code gets written.

Supporting both protocols matches the decision framework honestly, giving each job the protocol it actually needs. A team running a monitoring dashboard gets the simpler SSE path, with reconnection and scaling handled underneath. A team building something closer to collaborative editing or an order-entry screen that genuinely needs two-way, high-frequency traffic gets WebSocket without having to bolt it on as an afterthought or fake it with workarounds. Nobody gets stuck forcing a one-way problem through a two-way tool, or the reverse.

The authentication wrinkle raised earlier, EventSource's lack of support for custom headers, is exactly the kind of practical friction a platform like this exists to absorb, whether through cookie-based auth as the cleanest route or a short-lived token exchange before the connection opens. It's a small detail next to the bigger architectural question, but it's the kind of detail that eats an afternoon when a team hits it cold on their own. The point of the whole framework above is to make that choice once, correctly, based on what the dashboard actually needs to do, not on which protocol happened to be the reflex answer that week.

Notes

  1. Server-Sent Events: A WebSockets alternative ready for another look

    Provided the LinkedIn case study on scaling SSE to hundreds of thousands of persistent connections on a single machine.

  2. Using Server Sent Events to Simplify Real-time Streaming at Scale - Shopify

    Provided the Shopify Black Friday/Cyber Monday Live Map example, including uptime, latency figures, and comparison with the 2021 polling system.

Julian Marroquin

Reporter

Known for clear, grounded explainers, Julian Marroquin handles API, RPC API, Representational State Transfer at Interfacer.

← Back to Interfacer