Server-Sent Events (SSE) is a one-way streaming protocol where the server
holds a single HTTP response open and pushes a sequence of text events to
the client, which reads them through the browser's
EventSource API. To debug SSE / EventSource streams you watch
three things: the response headers that put the connection into streaming
mode, the individual events as they arrive, and how the stream finally
ends. The workflow below covers the on-the-wire format, the proxy and
encoding problems that silently break streaming, and how to read a live
stream in practice.
What to inspect when you debug an SSE / EventSource stream
A complete SSE debugging pass looks at four layers, in order:
- The response headers -
Content-Type: text/event-streamplusCache-Control: no-cache. If the content type is wrong, the browser never treats the body as an event stream and no events fire. - The events - the
data,event,id, andretryfields the server actually writes, plus the blank line that dispatches each event. - Reconnection state - the last
idthe client saw and theretryinterval, which decide what happens after a drop. - How the stream ends - whether the server closed cleanly or the connection was cut mid-event, which is often the single most useful clue.
How Server-Sent Events work on the wire
Most streaming bugs are easier to read once you know what the bytes mean. SSE is defined by the WHATWG HTML specification, and the client API is documented on MDN.
One long-lived HTTP response
An SSE stream starts as an ordinary GET request with
Accept: text/event-stream. The server replies
200 OK with Content-Type: text/event-stream and
then never closes the body - it keeps writing into the same response for
as long as the connection lasts. Over HTTP/1.1 the body is delivered with
chunked transfer encoding; over HTTP/2 it is a stream that stays open. The
payload must be sent uncompressed: SSE is a live text stream, so an
identity encoding is the norm and a gzip/Brotli layer is what
breaks incremental delivery.
The event stream format
The body is UTF-8 text parsed line by line. Each non-empty line is a
field: value pair, a leading : marks a comment
(often a keepalive), and a single optional space after the colon is
stripped. Four fields are defined:
| Field | Purpose |
|---|---|
data | The payload; repeat it to build a multi-line value joined with \n |
event | Names the event so the client can listen for a custom type; defaults to message |
id | Sets the event ID the client remembers for reconnection |
retry | Reconnection delay in milliseconds (digits only) |
The detail that trips people up is dispatch: an event is only delivered
when the parser hits a blank line. A block with
data lines but no trailing empty line sits in the buffer and
the client sees nothing. A leading UTF-8 byte order mark is stripped once,
and unknown fields are ignored rather than rejected.
GET /events HTTP/1.1
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
: keepalive comment - ignored by the client
event: message
data: {"text":"Review Summary"}
id: 42
event: progress
data: {"done":30}
data: {"total":100}
retry: 3000 Reconnection: id, Last-Event-ID, and retry
Reconnection is built into the protocol, which is the main reason teams
pick SSE over a raw socket. Whenever the server sends an id,
the client stores it; if the connection drops, the browser reconnects
automatically and replays the stored value in a
Last-Event-ID request header so the server can resume from
where it left off. The retry field overrides the default
reconnection delay. The catch worth debugging: replay only works if the
server actually honors Last-Event-ID - many deployments send
IDs but never resume, so events are silently lost across a reconnect.
SSE vs WebSocket
SSE is server-to-client only and rides plain HTTP, so it passes through proxies, CDNs, and load balancers without a protocol upgrade and brings automatic reconnection for free. WebSocket is full-duplex but needs an upgrade handshake and its own reconnection logic. For live dashboards, notifications, log tails, and token-by-token AI/LLM responses - anywhere data flows one way - SSE is simpler and easier to debug.
Why SSE streams are hard to debug
Most "my EventSource never fires" reports are not application bugs - they are something between the server and the client buffering or rewriting the stream. The usual suspects:
- Proxy buffering. Nginx buffers proxied responses by
default, so events pile up in the buffer and the client sees nothing for
minutes; you need
X-Accel-Buffering: noorproxy_buffering off. CDNs such as Cloudflare buffer too unless the route streams. - A missing blank line. Forgetting the second newline after an event is the most common SSE bug - the event never dispatches and the stream looks dead.
- The wrong Content-Type. Anything other than
text/event-streamin the response HTTP headers means the browser does not parse the body as events. - Compression in the path. A gzip or Brotli layer added by the server or a proxy buffers the stream to compress it and defeats incremental delivery.
- CORS. A cross-origin stream needs the right
Access-Control-Allow-Origin; the connection fails before the first event with only a genericerrorevent on the client. - The HTTP/1.1 connection cap. Browsers allow about six connections per origin; several open SSE streams plus normal requests can exhaust the pool. HTTP/2 multiplexing removes the limit.
The hard part is that the browser's error event is opaque -
it tells you the connection failed, not why. You need to see the raw
stream on the wire to tell a buffering proxy from a missing newline from a
truncated response.
Ways to debug an SSE stream
| Method | Good for | Limitation |
|---|---|---|
| Browser DevTools (EventStream tab) | A page you load yourself in the browser | Browser tabs only; Chrome's view is bare and shows nothing for non-browser clients |
curl -N | Confirming the raw bytes and headers from the shell | Manual, no per-event view, no decryption of another app's HTTPS |
| System-wide capture (HTTP Debugger) | Any process, including HTTPS, localhost, and HTTP/2 | Windows only; inspects rather than modifies the stream |
From the shell, the fastest first check is to read the stream directly:
curl -N -H "Accept: text/event-stream" https://api.example.com/events
The -N flag disables curl's own output buffering, so events
print as they arrive. If they appear here but not in your app, the problem
is on the client; if they stall here too, it is the server or something
buffering in between.
How to debug an SSE stream step by step
- Confirm the response headers.
Verify the response is
200withContent-Type: text/event-streamand no compression. A wrong content type or aContent-Encoding: gzipheader explains a stream that never starts. - Watch events arrive in real time. Follow events on a timeline as they stream in. If the headers look right but no events appear, suspect proxy buffering or a missing blank line between events.
- Read each event's fields and payload.
Check the
eventtype,id, and thedatapayload - pretty-printed when it is JSON - so you compare application data, not a wall of raw text. - Check reconnection and how the stream ends.
Confirm the
id/Last-Event-IDandretrybehavior across a reconnect, and check whether the stream closed cleanly or was cut off mid-event.
In the browser, Firefox and Chrome DevTools expose received events under the Network tab when you select the stream request. That works for pages you load yourself. It does not help when the stream is consumed by a desktop app, a background service, a CLI tool, a mobile emulator, or one backend calling another - and it cannot show a stream that opened before DevTools was attached.
Debugging SSE events in HTTP Debugger
HTTP Debugger Pro captures Server-Sent Events system-wide on Windows without a proxy or browser extension, so it sees streams from any process - browsers, desktop apps, services, CLI tools, and AI agents - including encrypted HTTPS, localhost, and HTTP/2. Because capture is not tied to a browser tab, it also catches a stream that was already open before you started looking.
A response with Content-Type: text/event-stream is marked
as an SSE stream in the main grid, and each connection gets its own
Server-Sent Events view: a timeline of events as they
arrive, with a sequence number, direction, time offset, event type,
byte size, and a payload preview. You can filter by event type or
direction and search across payloads.
For each event, the Text tab shows the
data payload as readable text and auto-formats it when it
is JSON, with Raw and Hex views
alongside. The Raw view reconstructs the id,
event, and data lines exactly as they came off
the wire, so you can confirm the field structure the server actually
sent.
The Info tab breaks out each event's direction, type, time offset, and byte size, and the stream's summary records whether it ended cleanly or was truncated - the signal that separates a normal close from a connection that was cut mid-event. SSE traffic is read-only here: HTTP Debugger inspects and decodes events rather than injecting or rewriting them.
It complements browser tooling: use DevTools for a page you control, and a system-wide HTTP sniffer when the stream lives outside the browser or rides encrypted HTTP traffic you cannot attach DevTools to. The same connection's response headers and events sit next to the rest of your captured session, which makes it a practical HTTP analyzer for real-time protocols like WebSocket and gRPC.
Common mistakes when debugging SSE
- Blaming the client for a buffering proxy. If the events stall on the wire too, the client is fine - look at Nginx, a CDN, or any hop that buffers the response.
- Forgetting the blank line. An event without its trailing empty line never dispatches; the stream looks alive but the client gets nothing.
- Compressing the stream. A gzip or Brotli layer turns a live stream into a buffered one and stops incremental delivery.
- Trusting Last-Event-ID without server replay. Sending
idvalues does nothing unless the server resumes from theLast-Event-IDheader on reconnect. - Debugging only in the browser. DevTools cannot see SSE from desktop apps, services, backend-to-backend calls, or a stream opened before it attached.
FAQ
How do I view the events in an SSE stream?
Open the stream request in your browser's Network tab to read
received events, or use curl -N -H "Accept:
text/event-stream" URL from the shell to print them as they
arrive. For a stream from a non-browser process, use a system-wide
capture tool that lists each event with its type, size, and payload.
Why is my SSE connection hanging with no events?
The connection opens but no events arrive almost always means
buffering or framing. Check that a proxy is not buffering the
response (disable Nginx proxy_buffering or set
X-Accel-Buffering: no), that each event ends with a
blank line, and that nothing in the path compresses the stream.
What is the difference between SSE and WebSocket?
SSE is one-way, server-to-client over plain HTTP with automatic reconnection built in. WebSocket is full-duplex but needs an upgrade handshake and its own reconnection logic. Use SSE when data flows in one direction; use WebSocket when both sides need to send.
Can I debug SSE over HTTPS or from a non-browser app?
Yes. Browser DevTools only sees streams from pages it is attached to. For desktop apps, services, CLI tools, or backend-to-backend calls, use a system-wide capture tool such as HTTP Debugger Pro that decrypts HTTPS with a local root certificate and shows events from any process on the machine.
Does SSE work over HTTP/2?
Yes. SSE is plain HTTP, so it runs over HTTP/2, where stream multiplexing removes the roughly six-connection-per-origin limit that HTTP/1.1 imposes on multiple open streams.