TT Lab
Get started
Learn Learning paths Courses

SSE — How the Server Speaks First

Why SSE Fits More Often Than WebSocket

Continue in TT Lab

In one line

SSE is an HTTP response that never ends. It is not a new protocol, so proxies, authentication, reconnection, and logging all keep working as they are.

What's different

SSE WebSocket
Protocol Plain HTTP Separate after Upgrade
Direction Server → client Bidirectional
Reconnection The browser does it automatically Build it yourself
Resuming what was missed Last-Event-ID is built in Build it yourself
Authentication Cookies and headers work as they are Must be layered onto the handshake
Proxy/LB Existing configuration works Separate configuration needed
Data Text (UTF-8) Text + binary

If the client doesn't need to keep talking to the server, SSE is almost always the right choice. Notifications, progress, dashboard updates, and LLM token streaming all fit. User input can be sent as a plain POST.

Wire format

The header is Content-Type: text/event-stream, and the body looks like this.

event: token
id: 42
data: 안녕

data: 여러 줄이면
data: data: 를 반복한다

: 이건 주석이다. 하트비트로 쓴다

retry: 3000

The rules fit in five lines.

The browser side is three lines

const es = new EventSource("/stream")
es.addEventListener("token", e => append(e.data))
es.onerror = () => { /* 브라우저가 알아서 다시 붙는다 */ }

If the connection drops, it reconnects automatically and sends the last id it received in the Last-Event-ID header. If the server sends from the event after that id, the user never notices the drop. To do the same with WebSocket, you have to build reconnection, deduplication, and ordering yourself.

So why doesn't it work — buffering

Most of the problems you actually hit with SSE come down to one.

The code is correct, but everything appears on screen at once.

Someone in the middle is collecting the response. There are three culprits.

1. The reverse proxy. nginx buffers responses by default.

proxy_buffering off;              # location 에

Or turn it off from the app with a header — X-Accel-Buffering: no.

2. Compression. gzip has to collect data in blocks to compress. With Content-Encoding: gzip attached, streaming is effectively dead. Exclude SSE responses from compression.

3. The framework. This is the case where code that builds the whole response and returns it at once is mistaken for streaming. If you build a list and return it instead of using yield in a generator, it is just one big response.

Diagnose by time. Connect with curl -N and look at when the first byte arrives. If it arrives only after everything has been built, something is collecting it. -N is the option that turns off curl's own buffering.

Without a heartbeat, the connection drops

Load balancers and proxies have an idle timeout (usually 60 seconds). If no bytes flow, they cut the connection. So you periodically let a comment line through.

: ping

The client doesn't treat this as an event. Only bytes flow. An interval of 15–30 seconds is enough.

Connection limit

Under HTTP/1.1, browsers limit concurrent connections to 6 per origin. One SSE stream permanently holds one of them. Open six tabs and every request from the seventh tab stalls. The problem disappears with HTTP/2 (multiplexing). If you use SSE in production, HTTP/2 is not optional.

What gets forgotten on the server side

The generator keeps running even after the client disconnects — unless it notices and stops. Every time a user closes a tab, one more zombie task piles up on the server.

async def gen():
    try:
        while True:
            yield {...}
    finally:
        await cleanup()      # 끊길 때 반드시 여기로 온다

Adding finally should become a habit.

The order in which to diagnose in the field

When a report comes in that "the code is correct but everything appears on screen at once," before touching the server code, first work out how far it flows.

  1. curl -N http://앱주소/stream (the placeholder is the app address) — connect directly to the app. -N is the option that turns off curl's own output buffering. If you leave it out, the tool makes you suspect the server
  2. Make the same request once more through the proxy. If it clumps up only here, the culprit is buffering or compression
  3. The EventStream view in the browser developer tools Network tab — check whether events arrive one at a time

Narrowing it down one step at a time settles within minutes whether the server, the proxy, or the client needs fixing. When diagnosing, the first thing to suspect is the measuring tool itself.

When to use WebSocket

Otherwise, SSE is much cheaper to operate. Choosing by what gets recovered automatically when the connection drops usually gives the answer.