What are server-sent events?
TL;DR
Server-sent events (SSE) is a standard that allows a web page to receive automatic updates from a server via an HTTP connection. Server-sent events are used with EventSource instances that open a connection with a server and allow the client to receive events from the server. Connections created by server-sent events are persistent (similar to the WebSockets), however there are a few differences:
| Property | WebSocket | EventSource |
|---|---|---|
| Direction | Bi-directional – both client and server can exchange messages | Unidirectional – only server sends data |
| Data type | Binary and text data | Only text |
| Protocol | WebSocket protocol (ws://) | Regular HTTP (http://) |
Creating an event source
const eventSource = new EventSource('/sse-stream');
Listening for events
// Fired when the connection is established.eventSource.addEventListener('open', () => {console.log('Connection opened');});// Fired when a message is received from the server.eventSource.addEventListener('message', (event) => {console.log('Received message:', event.data);});// Fired when an error occurs.eventSource.addEventListener('error', (error) => {console.error('Error occurred:', error);});
Sending events from server
const express = require('express');const app = express();app.get('/sse-stream', (req, res) => {// `Content-Type` need to be set to `text/event-stream`.res.setHeader('Content-Type', 'text/event-stream');res.setHeader('Cache-Control', 'no-cache');res.setHeader('Connection', 'keep-alive');// Each message should be prefixed with data.const sendEvent = (data) => res.write(`data: ${data}\n\n`);sendEvent('Hello from server');const intervalId = setInterval(() => sendEvent(new Date().toString()), 1000);res.on('close', () => {console.log('Client closed connection');clearInterval(intervalId);});});app.listen(3000, () => console.log('Server started on port 3000'));
In this example, the server sends a "Hello from server" message initially, and then sends the current date every second. The connection is kept alive until the client closes it.
What are Server-Sent Events?
Server-Sent Events (SSE) is a standard that allows a server to push updates to a web client over a single, long-lived HTTP connection. It enables real-time updates without the client having to constantly poll the server for new data.
How SSE works
-
The client creates a new
EventSourceobject, passing the URL of theserver-sidescript that will generate the event stream:const eventSource = new EventSource('/event-stream'); -
The server-side script sets the appropriate headers to indicate that it will be sending an event stream (
Content-Type: text/event-stream), and then starts sending events to the client. -
Each event sent by the server follows a specific format, with fields like
event,data, andid. For example:event: messagedata: Hello, world!event: updateid: 123data: {"temperature": 25, "humidity": 60} -
On the client-side, the
EventSourceobject receives these events and dispatches them as browser events, which can be handled using event listeners or theonmessageevent handler:eventSource.onmessage = function (event) {console.log('Received message:', event.data);};eventSource.addEventListener('update', function (event) {console.log('Received update:', JSON.parse(event.data));}); -
The
EventSourceobject automatically handles reconnection if the connection is lost, and it can resume the event stream from the last received event ID using theLast-Event-ID HTTP header.
SSE features
- Unidirectional: Only the server can send data to the client. For bidirectional communication, web sockets would be more appropriate.
- Retry mechanism: The client will retry the connection if it fails, with the retry interval specified by the
retry:field from the server. - Text-only data: SSE can only transmit text data, which means binary data needs to be encoded (e.g., Base64) before transmission. This can lead to increased overhead and inefficiency for applications that need to transmit large binary payloads.
- Built-in browser support: Supported by most modern browsers without additional libraries.
- Event types: SSE supports custom event types using the
event:field, allowing categorization of messages. Last-Event-Id: The client sends theLast-Event-Idheader when reconnecting, allowing the server to resume the stream from the last received event. However, there is no built-in mechanism to replay missed events during the disconnection period. You may need to implement a mechanism to handle missed events, such as using theLast-Event-Idheader.- Connection limitations: Browsers have a limit on the maximum number of concurrent SSE connections, typically around 6 per domain. This can be a bottleneck if you need to establish multiple SSE connections from the same client. Using HTTP/2 will mitigate this issue.
Implementing SSE in JavaScript
The following code demonstrates a minimal implementation of SSE on the client and the server:
- The server sets the appropriate headers to establish an SSE connection.
- Messages are sent to the client every 5 seconds.
- The server cleans up the interval and ends the response when the client disconnects.
On the client:
// Create a new EventSource objectconst eventSource = new EventSource('/sse');// Event listener for receiving messageseventSource.onmessage = function (event) {console.log('New message:', event.data);};// Event listener for errorseventSource.onerror = function (error) {console.error('Error occurred:', error);};// Optional: Event listener for open connectioneventSource.onopen = function () {console.log('Connection opened');};
On the server:
const http = require('http');http.createServer((req, res) => {if (req.url === '/sse') {// Set headers for SSEres.writeHead(200, {'Content-Type': 'text/event-stream','Cache-Control': 'no-cache',Connection: 'keep-alive',});// Function to send a messageconst sendMessage = (message) => {res.write(`data: ${message}\n\n`); // Messages are delimited with double line breaks.};// Send a message every 5 secondsconst intervalId = setInterval(() => {sendMessage(`Current time: ${new Date().toLocaleTimeString()}`);}, 5000);// Handle client disconnectreq.on('close', () => {clearInterval(intervalId);res.end();});} else {res.writeHead(404);res.end();}}).listen(8080, () => {console.log('SSE server running on port 8080');});
SSE vs WebSockets vs Long Polling
Most interview discussions of SSE start with the WebSockets comparison. To pick the right tool, you also need to know how SSE compares to the older long-polling pattern it largely replaced.
| Property | Long Polling | Server-Sent Events | WebSockets |
|---|---|---|---|
| Direction | Server to client (response per request) | Server to client (one connection, many messages) | Bidirectional |
| Transport | Plain HTTP | Plain HTTP (text/event-stream) | Upgraded TCP (ws:// or wss://) |
| Reconnect handling | Manual (reissue request) | Built-in (EventSource retries automatically) | Manual (write your own backoff) |
| Message format | Anything (JSON, etc.) | Text only (UTF-8) | Text or binary (frames) |
| Browser support | Universal | All evergreen browsers | All evergreen browsers |
| Server cost per connection | Low; connection closes between messages | Moderate; one TCP connection held open per client | Moderate; one TCP connection held open per client |
| Works through corporate proxies and firewalls | Yes (regular HTTP) | Usually yes (regular HTTP) | Sometimes blocked (Upgrade: websocket is rejected by some proxies) |
| Resume after disconnect | Manual | Built-in via Last-Event-ID | Manual |
A useful decision rule:
- For bidirectional, low-latency, frequent client-to-server messages (chat, multiplayer, collaborative editing), use WebSockets.
- For one-way notifications, dashboards, LLM streaming, or server progress updates, use SSE.
- To support very old clients or restrictive networks where neither works, fall back to long polling.
Modern uses of SSE: LLM streaming and beyond
SSE has become a common transport for LLM streaming. The fit is straightforward: completions stream tokens one at a time from server to client, the connection is one-way, and the protocol works over plain HTTPS through most corporate proxies.
- The OpenAI Chat Completions API returns
text/event-streamwhen called withstream: true. Eachdata:chunk contains a JSON delta with the next token. - The Anthropic Messages API uses the same pattern:
text/event-streamwithdata:chunks per delta. - Self-hosted servers like vLLM (which exposes an OpenAI-compatible API) also stream via SSE. Some others (such as Ollama) stream over newline-delimited JSON instead, so check the API docs before assuming SSE.
Beyond AI, SSE fits most "server has new data, push it to the user" use cases:
- Live notifications such as new messages, mentions, and alerts.
- Long-running job progress (deploys, exports, batch processing), where the server emits progress events as work proceeds.
- Live status pages and dashboards for build status or server health.
- Stock tickers and sports scores, where the client does not push data back.
When the use case is "client subscribes, server sends updates", SSE is usually simpler than WebSockets: you get auto-reconnect, message IDs, and HTTP/2 multiplexing for free.
Production gotchas with SSE
A few practical issues to be aware of:
- HTTP/1.1 connection limits. Browsers cap concurrent connections per origin to roughly 6 over HTTP/1.1. A tab that opens an SSE stream and also makes regular API calls can hit the limit quickly, especially when the user opens multiple tabs (each tab opens its own SSE). Running over HTTP/2 or HTTP/3 multiplexes streams over a single connection (typically up to 100 concurrent streams by default), avoiding the per-origin limit.
- Buffering proxies break streaming. Nginx, AWS ALB, and many reverse proxies buffer responses by default, so
data:chunks accumulate on the proxy and arrive in one large piece. Disable buffering on SSE routes:- Nginx: send
X-Accel-Buffering: nofrom the server, or setproxy_buffering offin the location block. - AWS ALB: response buffering is generally on; send data more frequently or use API Gateway with HTTP integration.
- Cloudflare: usually streams correctly, but check that the route is not behind "Auto Minify" or response transformations.
- Nginx: send
Last-Event-IDon reconnect. WhenEventSourcereconnects after a drop, it sends the lastid:it received in aLast-Event-IDrequest header. The server should use this to resume from the right point. Otherwise, reconnects either replay everything (causing duplicates) or skip events the client missed.- Authentication.
EventSourcedoes not let you set custom request headers, so there is noAuthorizationheader. Common workarounds: pass the token as a query string parameter (be careful, since this is visible in server logs), or use cookie-based auth (whichEventSourcedoes send). The newerfetch+ReadableStreamapproach lets you set headers if you need them. - Server timeouts. Many platforms (Heroku, App Engine, AWS Lambda via API Gateway) cap request duration. SSE connections can last for hours, which exceeds those limits. Either run on a platform that supports long-lived connections (Fly.io, Render, a self-hosted VPS) or have the client reconnect periodically.
Summary
Server-sent events provide an efficient and straightforward way to push updates from a server to a client in real-time. They are well-suited for applications that require continuous data streams but do not need full bidirectional communication, including the common pattern of streaming LLM responses. With built-in support in modern browsers, SSE is a reliable choice for many real-time web applications.