Many applications need the server to deliver updates as soon as they become available, while the client sends little or nothing through the same channel. A live score, deployment log, notification feed, progress indicator, or monitoring dashboard naturally follows this pattern.
Server-Sent Events, usually shortened to SSE, provides a simple way to stream those updates over HTTP. The client sends a normal HTTP request, and the server keeps the response open. Instead of returning one complete document, the server writes a sequence of text events over time.
SSE is deliberately one-directional:
If the client needs to create, update, or delete something, it uses an ordinary HTTP request alongside the event stream. This separation keeps SSE close to the normal HTTP model while still allowing low-latency server push.
An SSE connection is one long-lived HTTP response. The response begins normally with a status line and headers, but its body remains open while the server publishes events.
The POST is a separate HTTP exchange. It is not sent inside the SSE response.
This model has several useful properties:
EventSource.SSE is an application-layer event stream, not a different transport protocol. Depending on the negotiated HTTP version, it can occupy an HTTP/1.1 connection or an individual HTTP/2 or HTTP/3 stream.
A browser starts an SSE connection with an HTTP GET request. It may advertise the expected media type:
The server accepts the stream with a successful response:
The blank line after the headers begins the response body. The server does not send a Content-Length because it does not know the final body size and intends to keep producing data.
Content-Type: text/event-stream is essential. A browser EventSource expects a final HTTP status of 200 and this media type before it starts interpreting the body as events.
Cache-Control helps prevent caches from storing the stream and discourages intermediaries from transforming it. The exact cache configuration still needs to be verified at every proxy and CDN on the path.
Connection: keep-alive is specific to HTTP/1.1 and is not necessary for an HTTP/2 or HTTP/3 stream. It is commonly shown in HTTP/1.1 examples, but the behavior that matters is leaving the response body open.
No protocol upgrade occurs. All bytes remain part of an HTTP response:
HTTPS should be used in production so that credentials and event data are not visible or modifiable in transit.
An SSE response is UTF-8 text organized as lines. A blank line terminates an event.
Here is one complete event:
The blank line after data is part of the format. Once the browser reads it, it dispatches an event named order_status.
SSE defines four meaningful fields:
| Field | Meaning |
|---|---|
data | Adds text to the event payload |
event | Selects the event name; the default is message |
id | Sets the event ID used for reconnection |
retry | Sets the browser's reconnection delay in milliseconds |
A line beginning with a colon is a comment:
Comments do not create browser events. They are useful as heartbeats that put bytes on an otherwise idle connection.
Field names are case-sensitive. data is recognized, while Data is an unknown field and is ignored.
The first colon on a line separates the field name from its value:
This produces field name data and value hello:world. Colons after the first are part of the value.
If the value begins with one space immediately after the colon, that one space is removed:
Both produce test.
Only one optional space is removed. This line preserves one leading space in the value:
A field can omit the colon, in which case its value is empty:
That line has field name id and an empty value.
Unknown fields are ignored. This allows producers and consumers to tolerate additions without treating the stream as malformed.
An empty line tells the parser to dispatch the event accumulated so far:
This produces two events. A single newline only ends a field line; two consecutive line endings create the required blank line.
The end of the HTTP response does not dispatch an incomplete event. For example:
If the connection ends without the final blank line, the pending data is discarded. A producer must therefore finish every event with:
CRLF and CR line endings are also valid, but LF is straightforward and widely used.
data LinesAn event can contain several data fields:
The browser joins their values with newline characters. The delivered event.data is:
The final newline accumulated by the parser is removed before dispatch.
This behavior matters when sending JSON. A compact JSON document can use one line:
If a JSON string contains a logical newline, encode it as \n inside the JSON rather than writing an unprefixed physical line. Every physical payload line in an event stream needs its own data: prefix.
Without an event field, the browser dispatches a message event:
A named event uses the supplied event type:
Named events let a client register separate handlers without adding an application-level type field to every payload. A JSON type can still be useful when non-browser consumers share the same application contract.
Loading simulation...
EventSourceBrowsers expose SSE through the EventSource API:
The browser performs the HTTP request, decodes UTF-8, parses fields, combines data lines, dispatches named events, and reconnects when appropriate.
EventSource has three ready states:
CONNECTING also describes a stream that was open but is now waiting to reconnect. An error event therefore does not always mean the stream has permanently failed. Check readyState and expect transient disconnects.
To stop the connection and prevent future automatic reconnection:
The browser API always opens a GET request and does not accept a request body. It also does not provide a general option for application code to attach arbitrary request headers. Data that determines the subscription is commonly represented by the URL, an authenticated session, or server-side state.
Automatic reconnection is one of SSE's most useful built-in behaviors.
If an established event stream ends unexpectedly, the browser changes the EventSource back to CONNECTING, waits for its reconnection delay, and issues another request. The initial delay is implementation-defined and is typically a few seconds.
The server can update the delay by writing a retry field:
The value is a non-negative decimal integer measured in milliseconds. The line changes future reconnection timing; it does not dispatch an application event. Values containing other characters are ignored:
A browser may add more delay or backoff after failures to avoid repeatedly contacting an unavailable server. Applications should not rely on reconnection happening at an exact millisecond.
If the client calls close(), reconnection stops. A server can also return HTTP 204 No Content when it wants an EventSource to stop reconnecting permanently.
The id field gives an event a resume cursor:
After dispatching this event, the browser remembers 142. If the connection breaks, its next request includes:
The server can use that value to send events after 142:
IDs are strings, not necessarily integers. Their ordering and lookup meaning belong to the application.
The last event ID persists when later events omit id. An empty id field resets it:
After that block is processed, the browser no longer sends Last-Event-ID on reconnect until another non-empty ID is received.
Last-Event-ID only communicates a cursor. It does not make the server retain or replay events.
A resumable service needs an event store with a clear retention policy. On reconnect, the server must determine whether it can:
There is also an ambiguity around disconnection. The browser may have received an event while application code did not finish processing it, or the server may replay an event conservatively. Consumers should tolerate duplicates when correctness matters, commonly by storing the latest processed ID or making event handling idempotent.
SSE alone guarantees neither exactly-once delivery nor indefinite replay.
At a high level, an SSE handler follows this process:
For a structured event, the raw bytes might be created as:
The trailing blank line is mandatory. The server should write complete event records atomically from the application's perspective so concurrent producers do not interleave their field lines.
Every event must be authorized for that connection. A user permitted to subscribe to account 42 should not receive an event merely because it was published on a broadly shared process-level channel.
The server must observe when the request is canceled or a write fails, remove the connection from its subscription registry, and release queues and other resources.
A quiet disconnected client might not be detected immediately because no write is attempted. Periodic comments cause writes and help reveal dead paths:
Heartbeat frequency should be shorter than relevant proxy idle timeouts but not so aggressive that mostly idle connections create excessive traffic.
An application can generate an event immediately while the user receives it much later. The usual cause is buffering.
Data can wait in several places:
The endpoint should flush headers and flush after complete events. Framework response compression may wait for a useful compression block size, so it can delay small updates and should be tested or disabled for the stream.
Reverse proxies often buffer upstream responses by default. An SSE route usually needs response buffering disabled. In NGINX, for example, this can be configured with proxy_buffering off, and an upstream can request the behavior with:
That header is NGINX-specific, not part of SSE.
CDN caching and response transformations should be disabled for the route. Proxy read timeouts must also exceed the maximum period between bytes, or heartbeats must keep the path active.
Flushing is not a promise that every event becomes a separate TCP packet. Packet boundaries are irrelevant to the SSE parser. The goal is to prevent large application or intermediary buffers from holding data long enough to harm event latency.
An event producer can outpace a client. The server's writes then slow down or data accumulates in application, framework, kernel, proxy, and browser buffers.
The browser EventSource API does not expose a flow-control mechanism through which application code can ask the server to pause. Servers therefore need bounded per-connection queues and an explicit slow-consumer policy.
Depending on the data, a server can:
For an append-only audit feed, dropping data would be incorrect; disconnect-and-resume may be appropriate. For a CPU gauge updated ten times per second, retaining every obsolete intermediate reading may be wasteful.
Browser application code can also become a bottleneck. If event handlers perform expensive work faster than the main thread can sustain, tasks accumulate even when the network is healthy. Batch UI updates and keep handlers small.
An SSE endpoint is a read API that can continuously expose sensitive data. It requires authentication and authorization even though the client is not sending commands through the stream.
For a same-origin stream, a secure session cookie is a common authentication mechanism:
For a cross-origin stream that needs credentials:
The server must return valid CORS headers. For a credentialed request, it must allow the requesting origin explicitly and allow credentials:
The wildcard origin cannot be used with credentials.
Because EventSource does not let browser code add an arbitrary Authorization header, applications sometimes place tokens in the URL. URLs can appear in logs, history, monitoring systems, and referrer-related data, so long-lived bearer credentials should not be placed there casually. Prefer secure cookies, or use narrowly scoped, short-lived connection tokens with careful log redaction.
Authorization must be checked when the stream opens and reconsidered when permissions or session validity changes. A connection that has remained open for an hour should not retain access forever after the user is revoked.
Use event IDs that do not reveal sensitive global counts or permit one tenant to request another tenant's history. Treat Last-Event-ID as untrusted request input when selecting replay data.
With HTTP/1.1, each SSE response occupies one connection while it remains open. Browsers enforce per-origin connection limits, so several tabs or several independent EventSource objects can compete with ordinary requests for a small pool of connections.
HTTP/2 carries an SSE response on one multiplexed stream. Other requests and event streams can share the same underlying connection, and the maximum concurrent stream count is negotiated. HTTP/3 provides a similar multiplexed HTTP stream model over QUIC.
This does not remove application costs. Each subscriber still needs server-side state, buffers, authorization context, and event routing. Capacity planning should consider:
If multiple application instances serve SSE connections, each event must reach the instances that own interested subscribers. A shared event log or publish-subscribe layer commonly distributes updates between application instances. SSE defines delivery from an edge server to one client; it does not define the server-side fan-out system.
SSE has no protocol-level Close event equivalent. If the server ends the HTTP response, a normal EventSource usually attempts to reconnect.
This behavior is useful during a rolling deployment: an old instance can stop accepting new streams, finish or close existing responses, and allow clients to reconnect through the load balancer. Reconnection timing and server capacity must be planned so that thousands of clients do not overwhelm the replacement instances.
If a stream must end permanently, the browser application can call close(). A server that needs to tell an EventSource not to reconnect can return 204 No Content on a connection attempt.
An application-level event can announce planned maintenance or instruct application code to close, but that behavior is a convention created by the application, not a built-in SSE control frame.
An SSE endpoint can be inspected without a specialized protocol client. curl can show events as they arrive:
-N disables curl's output buffering. If resuming manually:
When the connection never opens, inspect the final HTTP status, Content-Type, authentication, CORS headers, redirects, TLS, and proxy route.
When events arrive in batches, inspect blank-line termination and every buffering layer. Compare the timestamp when the application writes and flushes an event with the timestamp at the proxy and client.
When a connection repeatedly drops, inspect proxy idle timeouts, heartbeat frequency, server exceptions, client network changes, and reconnection requests. Log connection IDs, event IDs, and disconnect reasons without logging sensitive payloads or credentials.
Browser developer tools can show the request, response headers, timing, and streamed event data. Remember that an indefinitely pending HTTP request is the expected healthy state.
SSE fits applications where updates primarily travel from server to client and text events are sufficient. Notification feeds, build logs, order-status changes, progress updates, dashboards, and live result streams are natural examples.
It is especially attractive when automatic browser reconnection, event IDs, and ordinary HTTP infrastructure simplify the design.
It is less suitable when both sides send frequent independent messages through one channel, binary messages are central, or the application requires transport features beyond a single ordered text stream. Client commands can still use ordinary HTTP, but a workload dominated by rapid bidirectional interaction may need a different communication model.
SSE is not repeated polling. One HTTP response remains open and delivers multiple events over time.
SSE is not a protocol upgrade. The event stream remains the body of an HTTP response.
SSE is one-way, not half of a hidden request channel. Client writes use separate HTTP requests.
A newline does not complete an event. A blank line is required to dispatch the accumulated fields.
The end of the response does not dispatch an incomplete event. Pending data without a final blank line is discarded.
SSE carries UTF-8 text, not native binary frames. Binary data requires a text encoding or, preferably, a separate binary-friendly endpoint.
id does not store events. It only gives the browser a cursor to send back as Last-Event-ID.
Automatic reconnection does not guarantee exactly-once delivery. The server and consumer must define replay, deduplication, and retention.
An error event is not always permanent failure. EventSource may already be waiting to reconnect.
Calling a server-side flush does not bypass every proxy buffer. The complete path must support streaming.
HTTP/2 reduces connection contention but not subscriber cost. Every active stream still consumes application and server resources.
A pending request is not evidence that SSE is stuck. A healthy stream is intentionally long-lived.
Server-Sent Events turns one long-lived HTTP response into an ordered stream of UTF-8 events. The client sends GET, and a successful endpoint returns 200 with Content-Type: text/event-stream. Events use data, event, id, and retry fields and end with a blank line; comments can act as heartbeats.
Browsers expose SSE through EventSource, dispatch named events, and reconnect automatically. Event IDs return as Last-Event-ID, but the server must implement replay and deduplication.
Production endpoints must manage buffering, idle timeouts, slow consumers, caching, authentication, CORS, and reconnect load. SSE fits primarily server-to-client updates when a text-based HTTP stream is sufficient.
SSE preserves the web's HTTP model while extending one response into a sequence of named events.
5 quizzes