skip to content

How does a handler that emits a Server-Sent Events stream differ from one that returns an ordinary JSON response?

level: juniorimportance: must knowfreq 60%

answer

  1. an ordinary GET, an endless answer
  2. nothing is upgraded or switched
  3. no length declared up front
  4. write each event when it happens
  5. handler returns only at stream end

basics

~20 s

An event-stream handler never finishes its response. It answers with 200 OK and Content-Type: text/event-stream, declares no body length, writes each event as that event happens, flushes, and holds the response open instead of returning a document.

solid answer

~40 s

Nothing is negotiated or upgraded: it is a plain `GET` answered with `200 OK` and `Content-Type: text/event-stream`. The difference is entirely in the body. A JSON handler builds a complete value, the server learns its length, and the response is finished the moment the handler returns. An event-stream handler declares no `Content-Length`, because when the headers go out the body's length is unknown and is meant to stay unknown — the response is one the server simply never ends. It writes each event as the underlying thing happens, flushes so those bytes leave, and returns only when the stream is over. A handler that assembles the whole run and returns it is not a stream; it is a slow download that happens to be spelled in event-stream syntax.

code

http · 10 lines
http
GET /firings/8814/events HTTP/1.1
Host: kiln.example
Accept: text/event-stream

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache

event: stage
data: {"stage":"bisque-ramp","targetC":980}

go deeper

for a junior

Recall the three visible facts: an ordinary GET, a 200 OK carrying Content-Type: text/event-stream, and a body the server keeps writing to instead of finishing.

for a middle

Explain why no length is declared and why the handler cannot build the body first — the length is unknown at header time and the response is meant to have no planned end.

for a senior

Show that you have counted the cost: an occupied response holds a connection slot and a live subscription for as long as the feed lasts, which is what every other duty on this surface exists to manage.

for a principal

The angle worth arguing is when a feed deserves an occupied response at all, against polling or a duplex channel, given what each costs a fleet in held connections.

## An ordinary response that never ends Server-Sent Events adds no new protocol, port or negotiation step. The receiving side issues a normal `GET`, usually with `Accept: text/event-stream`, and the emitting side answers `200 OK` with `Content-Type: text/event-stream`. Everything unusual about the exchange lives in what happens after those headers: the body is written incrementally, for as long as the feed lasts, and the response is not complete until the emitting side decides it is. That is the whole shift a first-time implementer has to make. A request/response handler produces a value; an event-stream handler occupies a response. ## What goes out when the stream opens - **`200 OK`** — an ordinary success status. No status is switched, nothing is upgraded, and a stem that talks about "the handshake" on an event stream is describing a different protocol. - **`Content-Type: text/event-stream`** — this is what tells a conforming client to parse the body as a live sequence of events rather than as a document. - **No `Content-Length`** — the length is unknown when the headers are sent and is meant to stay unknown. Declaring one is a promise the emitting side cannot keep and cannot revise. - **`Cache-Control: no-cache`** is conventional, so nothing on the path is tempted to serve a stored copy of a feed. - **No body at all yet.** The client considers the stream open as soon as a successful response with the right media type arrives; events follow whenever there is something to say. ## Why the length is never declared | | a document response | an event-stream response | |---|---|---| | when the body is known | fully, before headers go out | never; it grows for the life of the response | | declared length | yes | none | | when bytes leave | once, at completion | per event, as each event happens | | when the handler returns | after producing the value | only when the stream ends | | what "done" means | the value was delivered | the emitting side, or the receiver, ended the response | How a body of unknown length is framed on the wire is the HTTP version's business, not this protocol's: HTTP/1.1 uses a transfer coding for it, while HTTP/2 and HTTP/3 frame bodies themselves and have no such header field. The duty here is the same on all of them — never declare a length, never accumulate the body. ## The shape of the handler 1. **Send the headers** — status, media type, and whatever cache directives the feed needs. After this point the response is open and the client is listening. 2. **Loop for as long as the feed lasts.** Each time the underlying source has something to report, write that event's lines. 3. **Flush after each event's terminating blank line**, so the bytes actually leave rather than sit in a buffer waiting for more. 4. **Return only when the stream is over** — when the work being reported finishes, when the emitting side has decided the response has lived long enough, or when the receiver has gone. Consider a kiln monitor for a fourteen-hour firing. Roughly every few minutes a stage changes — ramp, soak, cool — and between those moments there is nothing at all to report. The response opens in the first second of the firing and is still open thirteen hours later, having carried perhaps two hundred events. Every one of them was written the moment it happened, which is the only reason anyone watching learned about it then. ## What this costs, and why juniors get asked it Because the response is occupied for the whole feed, it holds resources for the whole feed: a connection slot, whatever the server's concurrency model attaches to an in-flight response, and the subscription to the source behind it. That is the trade this protocol makes, and it is why the emitting side's other duties — flushing, keeping a silent response credible, noticing a receiver that left — exist at all. The question is a screening question because the failure it catches is so common: a handler that collects events into a list, returns the list at the end, and produces a perfectly valid event-stream body that nobody sees until the firing is over. The syntax is right and the behaviour is the opposite of what was asked for. A last point of vocabulary: what is held open here is **one HTTP response body**, and the client remains free to open others. That is a different cost model from a duplex socket, where one connection is one channel, and from a multiplexed connection carrying many concurrent calls.

  • Does an event-stream response ever declare how long its body is?
    No. When the headers are sent the emitting side does not know how many events the feed will carry, and the point of the response is that it has no planned end. It sends no `Content-Length`; how a body of unknown length is framed is left to the HTTP version in use.
  • What HTTP status and media type does the emitting side send when the stream opens?
    `200 OK` with `Content-Type: text/event-stream`, usually alongside `Cache-Control: no-cache`. It is an ordinary successful response — no status is switched and no protocol is upgraded, which is exactly why any HTTP client can read the result.

saying these in an interview costs you the question

  • Collects every event and returns them together when the feed ends.
  • Sets a Content-Length on an event-stream response.
  • Says the stream needs a special HTTP status rather than 200 OK.
  • Describes an upgrade or handshake step that this protocol does not have.
  • Treats the handler as a call that returns once, not a response held open.