LS LOGICIEL SOLUTIONS
Toggle navigation

What Is Server Sent Events?

Definition

Server-sent events, usually shortened to SSE, is a web standard that lets a server push a continuous stream of text-based updates to a client over a single, long-lived HTTP connection. The client opens the connection once, using a standard HTTP GET request, and the server keeps it open, sending new messages whenever it has something to say, without the client needing to ask again each time. It's a one-way channel, server to client only, which makes it simpler than a fully bidirectional protocol while still solving the core problem of pushing live updates without polling. That narrower scope is a deliberate design choice, not a limitation someone forgot to fix; it's what keeps SSE simple enough to implement with plain HTTP infrastructure that already exists everywhere, rather than requiring a dedicated protocol upgrade and its own supporting infrastructure. It's defined as part of the HTML5 specification, with a matching `EventSource` API built into every modern browser.

The reason server-sent events exist is that a lot of real-time needs are genuinely one-directional: a client wants to receive updates as they happen, but doesn't need to send much back over that same channel. Live notifications, a stock ticker, a progress indicator for a long-running job, or a stream of tokens from an AI model generating text all share this shape. Before SSE, developers handled this with polling, repeatedly asking the server "anything new?" on a timer, which wastes bandwidth and adds latency, or with WebSocket, which solves the problem but brings along a full-duplex protocol and its operational complexity for a need that never actually required two-way communication. SSE exists to fill that specific gap: real-time push, without the overhead of a whole separate protocol. A team building a notification feed doesn't need a bidirectional channel just to receive updates, and reaching for one anyway means carrying complexity, connection state management, a protocol upgrade handshake, a separate set of infrastructure concerns, that the actual feature never needed in the first place.

The mechanism is built entirely on ordinary HTTP: the client makes a GET request with an `Accept: text/event-stream` header, the server responds with that same content type and keeps the connection open, streaming a sequence of text-formatted event messages over time instead of ending the response after one chunk of data. What distinguishes SSE from either polling or WebSocket is this combination: it stays open like WebSocket does, avoiding repeated connection setup, but it flows in only one direction and speaks a format simple enough that a browser's built-in `EventSource` API handles reconnection and message parsing automatically, without a developer needing to write that logic themselves.

By 2026, server-sent events have found a particularly strong niche in AI-driven products, since streaming a language model's response token by token as it's generated, rather than waiting for the whole response and showing it all at once, is close to universal in modern AI interfaces, and SSE is a natural fit for exactly that pattern. Beyond AI streaming, SSE remains the quiet, practical choice for live notifications, activity feeds, and progress updates across a wide range of products, valued specifically because it's simpler to build, debug, and scale than WebSocket when the two-way channel WebSocket offers isn't actually needed. It rarely gets the same attention in engineering blog posts and conference talks that WebSocket does, in part because it's less novel and in part because when it's implemented well, it just works quietly in the background, which is generally a sign of a good infrastructure choice rather than a boring one.

This page covers how the SSE protocol and its message format actually work, how the browser's automatic reconnection behavior functions and where it falls short, how SSE compares practically to WebSocket and polling, where it fits and where a bidirectional protocol is genuinely required instead, and how a team implements SSE well in production. The idea underneath it is that matching the complexity of your real-time tool to the actual shape of your real-time need, one-way or two-way, tends to produce a system that's both simpler to run and easier to reason about, since every piece of protocol complexity you don't need is one less thing that can fail at 2am. Understanding it lets a team avoid reaching for WebSocket's full complexity when a simpler, one-directional stream would do the job just as well.

Key Takeaways

  • Server-sent events let a server push a continuous stream of updates to a client over one long-lived HTTP connection, in one direction only.
  • It's built on plain HTTP and a simple text format, with browsers handling reconnection automatically through the built-in EventSource API.
  • It solves live, one-way data needs, like notifications, activity feeds, and streaming AI output, without the complexity of a bidirectional protocol like WebSocket.
  • SSE's default HTTP/1.1 connection-per-stream model runs into browser connection limits, which matters for pages opening multiple simultaneous SSE streams.
  • It's the simpler, more appropriate choice than WebSocket whenever the client doesn't need to send frequent data back over the same channel.

The Protocol and Message Format

An SSE connection begins as a standard HTTP request with an `Accept: text/event-stream` header, and the server responds with a `Content-Type: text/event-stream` header of its own, along with instructions to avoid caching or buffering the response. Once that response starts, it never technically finishes in the traditional sense, the server keeps the connection open and continues writing to it over time, and the client's browser treats each new chunk of data as it arrives rather than waiting for the response to close.

The message format itself is deliberately simple, plain text lines rather than a binary or heavily structured format. Each event is a block of lines like `data: {"message": "hello"}` followed by a blank line marking the end of that event. Events can optionally include an `id:` field, letting the client track the last event it received, and an `event:` field, giving the message a named type so a client can listen for specific kinds of events rather than treating every message identically. This simplicity is deliberate: a developer can literally read an SSE stream in a terminal with a basic HTTP client and understand exactly what's being sent, without needing a specialized tool to decode a binary protocol.

On the client side, the `EventSource` API abstracts almost all of this away. A developer creates an `EventSource` pointed at a URL, registers a listener for incoming messages, and the browser handles opening the connection, parsing incoming event blocks, and delivering them as JavaScript events, without the developer needing to manually parse the raw text stream. This is meaningfully less code than a comparable WebSocket client needs to write, since WebSocket clients handle their own message framing and don't get this same level of browser-provided parsing for free.

The `id:` field paired with automatic reconnection is one of SSE's more practical design details. When a connection drops and the browser automatically reconnects, it sends a `Last-Event-ID` header back to the server containing the ID of the last event it successfully received, letting a well-built server resume the stream from where it left off rather than the client silently missing events that happened during the disconnect. This only works if the server actually implements and tracks event IDs meaningfully; if a server never sets the id field, reconnection still happens but the client has no way to know what it missed.

This resumption mechanism is one of the more underappreciated details in the whole SSE design, since it's handled entirely through ordinary HTTP headers rather than anything protocol-specific. A server that takes the time to track a rolling window of recent events, keyed by ID, can hand a reconnecting client exactly the messages it missed and nothing more, which is a meaningfully better experience than either replaying the entire history or silently dropping the gap. Getting this right takes deliberate server-side work, though; it's not something the EventSource API does automatically on the server's behalf.

Automatic Reconnection and Where It Falls Short

One of SSE's most practical advantages over building a similar system with raw HTTP or WebSocket is that the `EventSource` API reconnects automatically when a connection drops, without the developer writing any reconnection logic themselves. If a network hiccup, a server restart, or a proxy timeout closes the connection, the browser waits a short interval and reopens it, and this behavior is built into every modern browser's implementation rather than something each application has to reimplement.

This automatic behavior falls short in a couple of specific, worth-knowing ways. The reconnection delay is set by the server (through a `retry:` field in the stream) or defaults to a value on the order of a few seconds, and a server experiencing real trouble can end up flooded with a wave of simultaneous reconnection attempts from every disconnected client at once, since they all default to similar retry timing. Production SSE implementations that expect to serve many concurrent clients often need to have the server explicitly set varied or backed-off retry intervals to avoid this kind of reconnection stampede after an outage.

The other gap is that automatic reconnection handles the connection itself, but not necessarily application-level state. If a client's SSE connection to a live dashboard drops for thirty seconds and then reconnects, the browser will happily re-establish the stream, but unless the server implements the `Last-Event-ID` mechanism properly, whatever happened during those thirty seconds is simply lost, with no error, no warning, just a silent gap. Applications where missing events actually matters, financial data, critical alerts, need to build resumption logic deliberately rather than trusting that "the connection reconnected" means "no data was lost."

It's also worth knowing that `EventSource`, as originally specified, doesn't support setting custom headers on the initial connection request, which matters for authentication, since many APIs expect an `Authorization` header. Common workarounds include passing an auth token as a query parameter (with the security tradeoffs that implies, since URLs can end up logged in more places than headers, including access logs and browser history) or using a polyfill library that supports custom headers by using `fetch` under the hood instead of the native `EventSource` object. Teams building SSE into an authenticated product need to plan for this limitation rather than discovering it midway through implementation.

SSE Compared to WebSocket and Polling

The comparison to WebSocket comes down to directionality and complexity. WebSocket is fully bidirectional and requires its own protocol upgrade handshake, its own message framing, and generally more manual work to handle reconnection and message parsing on the client. SSE is one-directional, server to client only, runs over plain HTTP without any special upgrade, and gets automatic reconnection and message parsing largely for free through the browser's built-in API. When the actual need is genuinely one-way, live notifications, a progress bar, a streaming AI response, SSE gets there with less code, less new infrastructure, and less to reason about operationally.

The comparison to polling is more clearly in SSE's favor across the board for continuous updates. Polling, repeatedly making a fresh HTTP request every few seconds to check for new data, means the client sometimes gets a response with nothing new (wasted request) and sometimes waits longer than necessary for an update that already happened moments after the last poll (added latency). SSE removes both problems: the server sends data the instant it has something to send, and there's no wasted request overhead when nothing is happening, since the connection stays open in a genuinely idle state until there's real data to push.

One place SSE runs into a real, sometimes surprising limitation is the browser's cap on simultaneous HTTP/1.1 connections per domain, typically six. If a page opens several SSE connections to the same domain, perhaps for different live-updating widgets on one dashboard, it can hit this limit and find some connections stuck waiting. This is solved either by consolidating multiple data streams into a single SSE connection server-side, or by serving the API over HTTP/2, which multiplexes many streams over a single underlying connection and removes the per-domain connection cap that affects HTTP/1.1. Teams building SSE-heavy pages with multiple concurrent streams need to be aware of this before it shows up as a mysterious "some widgets never update" bug in production. It's a particularly easy trap to miss during development, since a developer testing locally rarely has enough other tabs or streams open against the same domain to hit the limit, and the bug only becomes visible once real users, often with several tabs of the product open at once, start reporting that some parts of the page seem frozen while others keep updating fine.

The practical decision, much like with WebSocket, comes down to whether the client genuinely needs to send data back frequently over the same channel. If it does, WebSocket is the right tool and SSE would need an awkward second channel (regular HTTP requests alongside the SSE stream) bolted on to handle the client-to-server direction. If it doesn't, SSE typically gets the job done with meaningfully less code and fewer new operational concerns than standing up a full WebSocket service would require.

Where Server-Sent Events Fit and Where They Don't

SSE fits well for live notifications and activity feeds, where a user wants to see new items appear without refreshing the page, and doesn't need to send anything back over that same channel to make it happen. It fits streaming AI-generated output particularly well, since a model generating text token by token maps naturally onto a stream of small SSE events, and this pattern has become close to standard in modern AI chat interfaces and coding assistants by 2026\. It fits progress updates for long-running background jobs, like a file processing pipeline or a report generation task, where the client just needs to watch percentage complete or status ticks up over time.

It fits live dashboards showing metrics or data that update periodically, stock prices, system health indicators, order counts, as long as the dashboard itself is read-only from the user's perspective and doesn't need to send frequent input back through that same connection. It's a comfortable middle ground between the wastefulness of polling and the added complexity of a fully bidirectional WebSocket connection for exactly this shape of one-way, continuous update.

It doesn't fit chat applications, multiplayer games, or collaborative editing tools, where the client needs to send data back just as often and just as urgently as it receives data. Trying to force SSE to handle these by pairing it with separate regular HTTP requests for the client-to-server direction usually ends up more complicated, not less, than simply using WebSocket, since now there are two separate communication mechanisms to keep synchronized instead of one unified connection.

It's also not the right tool for scenarios needing binary data streaming, like audio or video, since SSE's format is fundamentally text-based. And it's unnecessary overhead for data that genuinely doesn't need to be pushed at all, an admin settings page that a user might check once a day doesn't need a persistent connection of any kind, SSE or otherwise; a plain REST endpoint fetched on page load is simpler and sufficient.

Implementing Server-Sent Events Well

The first practical step is setting the response headers correctly and consistently: `Content-Type: text/event-stream`, `Cache-Control: no-cache`, and, depending on the server and any intermediate proxies, headers to disable response buffering, since a proxy or reverse proxy buffering the response defeats the entire point of a stream by holding data until it has a full chunk to forward, introducing exactly the delay SSE is meant to avoid. Teams deploying behind Nginx or similar reverse proxies specifically need to check and disable proxy buffering for SSE endpoints, since this is a common, easy-to-miss cause of an SSE stream that technically works but delivers everything in delayed bursts instead of in real time.

The second is implementing the `id:` field and a way to replay missed events on reconnection, from the start, rather than treating it as a nice-to-have added later. Even if the initial version of a feature doesn't need to worry much about a client missing a few seconds of updates, retrofitting reliable event IDs and replay logic onto a stream that's already been running in production, with its message format already relied upon by existing clients, is more disruptive than designing for it from day one.

The third is planning for connection scaling on the server side, since, like WebSocket, an SSE server holds open connections for as long as clients stay connected, and this means capacity planning has to account for concurrent open connections, not just requests per second. Unlike WebSocket, SSE connections don't need to route messages back the other direction, which simplifies the infrastructure somewhat, but a server still needs to track and hold state for every open stream and needs a strategy, often a pub/sub layer, for pushing data to the right connections when the source of new data lives on a different server instance than the one holding a particular client's connection.

The fourth is deciding deliberately, and documenting, how the server handles a wave of reconnections after an outage or deployment, setting a sensible and possibly randomized `retry:` interval so that clients don't all reconnect in the same instant and overwhelm a server that's just come back up. This is a small detail that's easy to skip during initial development, when there's only ever one client testing the stream, and easy to get burned by later, when a real outage causes thousands of clients to reconnect within the same few seconds.

Best Practices

  • Confirm the real need is genuinely one-directional before choosing SSE; if the client needs to send data back frequently, WebSocket is the better fit.
  • Set response headers correctly (text/event-stream, no-cache, disabled proxy buffering) so intermediate proxies don't silently buffer and delay the stream.
  • Implement the id field and Last-Event-ID handling from the start so clients can resume a stream without silently losing events after a disconnect.
  • Watch for the browser's per-domain HTTP/1.1 connection limit if a page opens multiple SSE streams, and consider HTTP/2 or stream consolidation to avoid it.
  • Set a sensible, possibly randomized retry interval so a server recovering from an outage isn't hit with every client reconnecting in the same instant.

Common Misconceptions

  • Server-sent events are not a form of WebSocket; they're a separate, simpler, one-directional protocol built entirely on plain HTTP.
  • Automatic reconnection in the EventSource API does not guarantee no data is lost; without the server implementing event IDs and replay, a gap during disconnection is silently skipped.
  • SSE is not limited to simple text notifications; it's widely used for streaming structured data, including token-by-token AI-generated responses.
  • A proxy or load balancer sitting in front of an SSE server does not automatically pass the stream through correctly; buffering has to be explicitly disabled or updates arrive in delayed bursts.
  • SSE connections are not free of scaling concerns just because they're "simpler than WebSocket"; they still hold open, stateful connections that require real capacity planning.

Frequently Asked Questions (FAQ's)

What is server sent events?

Server-sent events (SSE) is a web standard that lets a server push a continuous stream of updates to a client over a single, long-lived HTTP connection, flowing in one direction, from server to client, without the client needing to repeatedly ask for new data.

How is SSE different from WebSocket?

SSE is one-directional, server to client only, and runs over plain HTTP with automatic reconnection built into the browser's EventSource API. WebSocket is fully bidirectional, requires its own protocol handshake, and generally needs more manual client-side handling for reconnection and message parsing.

Does SSE reconnect automatically if the connection drops?

Yes, the browser's built-in EventSource API automatically attempts to reconnect after a disconnect. However, whether any events sent during the disconnect are recovered depends on the server implementing the id field and handling the Last-Event-ID header the browser sends when it reconnects.

Can SSE send binary data?

No, SSE's message format is text-based. Applications needing to stream binary data, like audio or video, need a different approach, such as WebSocket or dedicated streaming protocols, rather than SSE.

Why would we use SSE instead of just polling the server every few seconds?

Polling wastes requests when there's nothing new to report and adds latency since updates only arrive on the next scheduled check. SSE delivers data the instant the server has something to send and avoids the overhead of repeatedly opening new HTTP requests, since the connection stays open.

Is SSE a good fit for streaming AI-generated text?

Yes, it's one of SSE's strongest and most common use cases by 2026\. Streaming a language model's output token by token maps naturally onto a sequence of small SSE events, which is why it's widely used in AI chat interfaces and coding assistants.

What's the connection limit issue with SSE?

Browsers cap the number of simultaneous HTTP/1.1 connections per domain, typically around six. A page opening several SSE streams to the same domain can hit that limit, causing some streams to stall. This is usually solved by consolidating streams or serving the API over HTTP/2, which removes that per-domain cap.

Why does my SSE stream seem to deliver updates in delayed bursts instead of instantly?

This is almost always a proxy or reverse proxy buffering the response instead of passing chunks through immediately. Disabling proxy buffering for the SSE endpoint (a common configuration change in servers like Nginx) usually resolves it.

Can SSE send custom authentication headers with the connection?

The native EventSource API doesn't support setting custom headers on its initial request, which complicates token-based authentication. Common workarounds include passing a token as a query parameter or using a polyfill library built on fetch that supports custom headers instead of the native EventSource object.