skip to content

How do you build a structured SSE event with id, event name, and retry using SseEmitter, and how does the browser use those fields?

level: middleimportance: should knowfreq 33%

answer

  1. event() → SseEventBuilder
  2. name() → event: field, routes addEventListener
  3. id() → Last-Event-ID header on reconnect
  4. reconnectTime() → retry: reconnect delay
  5. data is always a string client-side

basics

~20 s

Use SseEmitter.event() to get an SseEventBuilder, then set .id(...), .name(...) (the event type), .reconnectTime(...) (retry) and .data(...), and pass it to emitter.send(). The browser's EventSource uses id for Last-Event-ID on reconnect, name to route to addEventListener, and retry as the reconnect delay.

solid answer

~40 s

SseEmitter.event() returns an SseEventBuilder that maps to the SSE wire fields: .id(String) → the id: line, .name(String) → the event: line (custom event type), .data(Object) → one or more data: lines (serialized via message converters), .reconnectTime(long) → the retry: line (reconnect delay in ms), and .comment(String) → a :comment line. You call emitter.send(builder). On the client, EventSource routes named events to es.addEventListener('name', ...) while unnamed ones hit es.onmessage. The id is remembered as lastEventId; on auto-reconnect the browser sends it back in the Last-Event-ID request header so the server can resume from where it left off. retry tunes how long the browser waits before reconnecting after a drop.

code

java · 25 lines
java
@GetMapping("/prices")
public SseEmitter prices(
        @RequestHeader(value = "Last-Event-ID", required = false) String lastId,
        TaskExecutor executor) {

    SseEmitter emitter = new SseEmitter();
    long start = (lastId == null) ? 0 : Long.parseLong(lastId) + 1;

    executor.execute(() -> {
        try {
            for (long id = start; id < start + 5; id++) {
                emitter.send(SseEmitter.event()
                        .id(String.valueOf(id))          // -> id:  (resumption)
                        .name("priceUpdate")             // -> event: (client routing)
                        .reconnectTime(3000)             // -> retry: 3000
                        .data(new Price("ACME", 42.10))); // -> data: {json}
            }
            emitter.complete();
        } catch (IOException e) { emitter.completeWithError(e); }
    });
    return emitter;
}
// Client:
// const es = new EventSource('/prices');
// es.addEventListener('priceUpdate', e => console.log(JSON.parse(e.data)));

go deeper

for a junior

Know SseEmitter.event() builds an event with id/name/data before send().

for a middle

Map each builder method to its wire field and know how the browser routes named events and parses data.

for a senior

Implement Last-Event-ID resumption and choose retry/heartbeat strategy consciously.

for a principal

Design a replayable event log (durable ids, at-least-once redelivery) behind SSE and reason about proxy/EventSource limits.

**The SSE wire format** is line-oriented UTF-8 text. An event is a group of `field: value` lines terminated by a blank line: ``` id: 100 event: priceUpdate retry: 3000 data: {"symbol":"ACME","price":42.10} ``` The defined fields are `id`, `event`, `data`, `retry`, and comment lines starting with `:`. **Building it in Spring — `SseEmitter.SseEventBuilder`.** `SseEmitter.event()` returns a builder with fluent methods: - `.id(String)` → `id:` — an opaque event identifier. - `.name(String)` → `event:` — the custom event **type** (the method is `name`, the wire field is `event`). - `.data(Object)` and `.data(Object, MediaType)` → `data:` — the payload; the object is serialized by a matching `HttpMessageConverter` (e.g. Jackson → JSON). You can call `.data(...)` multiple times to emit multiple `data:` lines. - `.reconnectTime(long millis)` → `retry:` — advises the browser how long to wait before reconnecting. - `.comment(String)` → a `:` comment line — ignored by clients, handy as a heartbeat/keep-alive. You then call `emitter.send(builder)`. (Calling `emitter.send(obj)` without the builder just emits a bare `data:` event with no id/name.) **How the browser (`EventSource`) uses each field:** - **`event` / `.name(...)`** — determines which listener fires. `es.addEventListener('priceUpdate', handler)` receives events with `event: priceUpdate`; events *without* an `event:` field go to `es.onmessage`. So naming events lets one stream multiplex several event types. - **`id` / `.id(...)`** — the browser stores it as `lastEventId`. On an automatic reconnect (EventSource reconnects on drop by default), it sends the stored value back in the **`Last-Event-ID`** HTTP request header. Your controller can read that header (`@RequestHeader(value = "Last-Event-ID", required = false)`) and resume the stream from the next event — this is the built-in **resumption** mechanism. - **`retry` / `.reconnectTime(...)`** — sets the reconnect delay (ms) the browser uses after the connection drops. Default is browser-defined (~3s); send `retry` to override. - **`data`** — delivered as `event.data` (a string) to the handler. If you sent JSON, the client does `JSON.parse(event.data)`. - **comment** — ignored by the client; purely to keep the connection warm. **Gotchas.** The wire field is `event` but the builder method is `name` — a common point of confusion. `data` is always a string on the client; structured data must be serialized (JSON) and parsed client-side. Multi-line data values are split into multiple `data:` lines and rejoined with `\n` by the client. `id` must not contain newlines. Resumption via `Last-Event-ID` only works if your server actually tracks/persists event ids and can replay — Spring gives you the header, but the replay logic is yours.

  • How does SSE support resuming after a dropped connection?
    The server sends id: on events; the browser remembers the last one and resends it as the Last-Event-ID request header on auto-reconnect. The server reads that header and replays from the next event — but the server must implement the replay/tracking itself.
  • Why does the builder method .name() correspond to the wire field event:?
    It's just Spring's naming: .name(...) sets the SSE 'event' field (the event type). Events with a name are dispatched to addEventListener(name); unnamed ones go to onmessage.

saying these in an interview costs you the question

  • Thinking .data() delivers a parsed object to the client (it's always a string; you JSON.parse it).
  • Believing SSE resumption works automatically without server-side id tracking/replay.
  • Confusing the builder method name() with an SSE 'name' field (the wire field is event:).
  • Assuming named events reach onmessage (they only reach matching addEventListener).

context