Lesson 0003 · ~10 minutes

Wire format, IDs, and resume

One skill: read a text/event-stream chunk the way the browser does, and know what reconnect actually guarantees.

Win for this lesson

In a design review you can say: “Auto-reconnect is free; at-least-once resume without gaps is a server contract using id + Last-Event-ID.”

1. The stream is plain text

The server keeps an HTTP response open with Content-Type: text/event-stream and writes UTF-8 lines. The format is defined in the WHATWG parsing section. An event is a block of fields ending in a blank line.

data: hello

data: multi-line starts here
data: second line

event: job-progress
id: 17
data: {"pct":40}

: keep-alive comment every ~15s

retry: 3000
data: after setting retry

Lines starting with : are comments (ignored) — useful as heartbeats so idle proxies do not kill the connection (authoring notes).

2. Field → MessageEvent

Wire Client sees
data: (one or more) event.data string; multiple lines joined with \n
no event: type messageonmessage / "message" listener
event: name type name → only addEventListener("name")
id: value updates last event ID; event.lastEventId on this and later events until changed
retry: ms sets UA reconnect delay (digits only); not exposed as a DOM event

One space after the colon is optional and stripped (data:x and data: x are the same value). Unknown field names are ignored.

3. The resume story (system design)

Two different promises people conflate:

Browser freebie

Connection drops → wait (default ~few seconds, or last retry:) → reconnect. If a last event ID is known, send request header Last-Event-ID. Spec: Last-Event-ID.

Your backend job

Read Last-Event-ID, decide what to emit next, keep a durable log or offset, stamp every event with a new id:. Without that, reconnect only means “subscribe from now” — gaps are possible.

Design review line

Ask: “Do we need gapless delivery after blips?” If yes, budget for an event store / cursor, not just EventSource. If “latest state is enough,” skip IDs and send full snapshots or accept gaps.

Empty id field (name present, empty value) clears the last event ID so the next reconnect omits the header — useful if you intentionally reset the cursor (see spec examples).

4. Practice A — read the wire

What does the browser dispatch for each finished block?

Block 1

Wire ends with a blank line:
data: alpha
data: beta
(blank)

Block 2

event: score
data: 3
(blank)
Which client handler runs?

Block 3

Server writes only:
: ping
(blank)

Block 4

Client received id: 7 then the TCP drop. On reconnect, what is true?

5. Practice B — design calls

Scenario A

Live “seats left” counter; showing a slightly old number after a 2s blip is fine. Resume strategy?

Scenario B

Trading blotter: every fill event must appear in order after reconnect. What do you require?

Scenario C

Events are rare; corporate proxy drops idle HTTP after ~30s. Cheap fix on the stream?

6. What to remember

Ask your teacher At-least-once vs exactly-once, multi-instance fans, or “fetch stream instead of EventSource” — ask while the wire is in working memory.

Primary source

WHATWG — Interpreting an event stream (field rules + worked examples). Readable companion: MDN Using server-sent events (fields table and reconnect notes).