skip to content

After a Server-Sent Events response drops, how does the client tell the server which events it already received?

level: middleimportance: must knowfreq 68%

answer

  1. the client keeps a bookmark
  2. one field out, one request field back
  3. the server is not obliged to honour it
  4. sent only when the remembered id is non-empty
  5. id: in the body, Last-Event-ID on the request

basics

~20 s

The server tags events with an id field; a client remembers the most recent value and, on reconnect, repeats the same GET with a Last-Event-ID request header carrying it, so the server can replay what was missed before resuming live.

solid answer

~40 s

Resumption is a two-part bargain. The emitting side writes `id:` on each event; a conforming client keeps the most recent value and, when the response drops and it re-opens the stream, re-issues the same request with `Last-Event-ID` set to that value, encoded as UTF-8. No application code is involved, which is why a Server-Sent Events feed looks self-healing. The second half is yours: the client is required to send the header, and the server is required to do nothing with it. If your handler ignores it and streams whatever happens next, the reconnect succeeds, the console looks healthy, and every gauge reading that arrived while the response was down is gone with no error anywhere. Replay from `Last-Event-ID` is a promise the emitter chooses to make.

code

http · 15 lines
http
GET /catchment/feed HTTP/1.1
Host: warnings.example
Accept: text/event-stream

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

id: 20480
event: level
data: {"gauge":"upper-weir","metres":3.41}

id: 20481
event: threshold
data: {"gauge":"upper-weir","crossed":"amber"}

go deeper

for a junior

Know the shape: events can carry an id, the client keeps the latest one, and it sends that value back in a Last-Event-ID request header when it re-opens the stream by itself.

for a middle

Explain both directions precisely and name which half the specification mandates: the client must send the header, the server is free to ignore it, so replay is code you write.

for a senior

Talk about the silent gap. Show how you make a resumed stream distinguishable from a merely reconnected one, and how you handle a first request that arrives with no header.

for a principal

The judgement is whether to promise resumability across a fleet at all: an id that survives instances and deploys is a data contract, not a header, and it constrains how the feed is stored.

## The round trip, in four steps A district warning console holds one Server-Sent Events response open against a catchment feed. Level readings and threshold crossings arrive as events. The link goes down for ninety seconds. Here is the whole resumption mechanism: 1. **The server labels.** Each event it dispatches carries an `id:` field whose value identifies that event in whatever store the events are read from. 2. **The client remembers.** As each event is dispatched the client copies the current id into its own last-event-ID state. This is a client-side value; nothing is acknowledged back to the server while the stream is healthy. 3. **The client re-opens.** After the reconnection delay, the client issues the same `GET` again — same URL, same `Accept: text/event-stream` — and adds one request header field, `Last-Event-ID`, whose value is the id it remembered, encoded as UTF-8. If its last event ID is empty, it sends no such field at all. 4. **The server decides.** Your handler reads that field, looks the value up, writes out the events that followed it, and then continues with live ones. Step four is the one the specification does not write for you. ## Who owes what | piece | direction | who requires it | |---|---|---| | `id:` on an event | server to client | nobody — events without an id are perfectly legal | | remembering the last id | inside the client | the specification, of a conforming client | | `Last-Event-ID` request field | client to server | the specification, whenever the remembered id is non-empty | | replaying from that value | inside the server | only you | Reading the table in the wrong direction is the classic error. `Last-Event-ID` is a **request** header field and never appears in the response; `id:` is a field inside the **response** body and is never sent by a client. And the client sends the header whether or not you ever intended to honour it — its presence proves nothing about whether replay works. ## What the client will not do for you The automatic behaviour ends at sending the field. In particular: - It does not detect a gap. A reconnect that resumes correctly and a reconnect that silently skips ninety seconds of readings look identical to the receiving application unless you say otherwise in the data. - It does not re-ask for events the server declines to replay, and there is no protocol-level way to demand them. - It does not negotiate how far back you can go. It sends one value; whatever you do with it is what happens. ## The silent-gap failure, and how to avoid shipping it The failure mode is not an error, and that is what makes it expensive. The console reconnects, events resume, dashboards tick, and the level trace for one gauge has a hole across exactly the interval a flood warning would have been raised in. Nothing logged an exception, because nothing failed. Two habits prevent it. First, **make the handler read `Last-Event-ID` on every request, including the first**, and treat its absence as "this client wants everything from now on" rather than as the normal case. Second, **make replay visible in the data**: a distinct `event:` name for replayed events, or a marker event before the live ones, so the receiving application can tell that it was caught up rather than merely connected. ## Ids the header can carry The value travels as a request header field value, which constrains it more than the body does. It is encoded as UTF-8 and it cannot contain a line break: a line ending is what terminated the `id:` field on the way out, so an id with an embedded newline was never expressible in the first place. A value containing a NULL character is ignored rather than stored, so it cannot come back either. Practically, keep ids short and printable — they ride in every reconnect request, from every client, for the lifetime of the feed. ## What this is not It is not an acknowledgement protocol: the client is not confirming receipt, and the server learns the value only when something has already gone wrong. It is not delivery guarantee machinery — nothing here makes an event exactly-once. And it is not the browser client's `error`-and-reopen behaviour, which is the same mechanism seen from the receiving application's side. On the wire there are only two moving parts: a field the server writes, and a request header field the client writes back.

  • Does the specification require the server to replay from the value it is given?
    No. The client must send `Last-Event-ID` when it holds a non-empty id; the server may do anything with it, including nothing. That asymmetry is the trap: the header arrives whether or not replay is implemented, so a feed can look resumable while losing every event that occurred during the outage.
  • The reconnect lands on a different instance of the service. Does resumption still work?
    The header travels with the request, so any instance receives it — but only an id that is meaningful beyond one process can be resolved. An id drawn from a per-process counter points at a different position on the new instance, which is worse than not resuming at all, because the replay looks successful.

A bookmark left in a ledger: you note the number of the last line you read, and when you come back you hand that number over and ask for everything after it. Whether the older pages are still on the shelf is up to the keeper, not to your bookmark.

saying these in an interview costs you the question

  • Thinks Last-Event-ID is a response header the server sends.
  • Believes the client acknowledges each event id while the stream is healthy.
  • Assumes replay happens automatically once the header is present.
  • Expects the receiving application to notice a gap on its own.
  • Thinks application code must implement the reconnect and the header.