skip to content

What is SseEmitter in Spring MVC, and how do you use it to stream Server-Sent Events to a browser?

level: juniorimportance: must knowfreq 55%

answer

  1. text/event-stream + EventSource
  2. extends ResponseBodyEmitter
  3. send() per event, complete() at end
  4. returns immediately, async request
  5. onCompletion / onTimeout / onError

basics

~20 s

SseEmitter is a Spring MVC return type that lets a controller push multiple messages to the client over one open HTTP connection as Server-Sent Events (text/event-stream). You return it immediately, then call emitter.send(...) for each event and emitter.complete() when done.

solid answer

~30 s

SseEmitter is a specialization of ResponseBodyEmitter for the Server-Sent Events protocol. A controller method returns an SseEmitter, and Spring keeps the HTTP response open with Content-Type text/event-stream. You then call emitter.send(data) — or emitter.send(SseEmitter.event()...) to set id/event/retry fields — to push each event; the browser's EventSource receives them incrementally. Call emitter.complete() to close cleanly or completeWithError(ex) on failure. The controller returns immediately, and the actual sending usually happens on another thread (e.g. from an @Async method or scheduler), because the servlet request thread is released via Spring's async request processing. Typical uses: live progress, notifications, dashboards, LLM token streaming.

code

java · 34 lines
java
@RestController
public class ProgressController {

    private final TaskExecutor executor;

    public ProgressController(TaskExecutor executor) {
        this.executor = executor;
    }

    @GetMapping("/progress")
    public SseEmitter streamProgress() {
        SseEmitter emitter = new SseEmitter(60_000L); // 60s timeout

        emitter.onTimeout(emitter::complete);
        emitter.onError(ex -> emitter.completeWithError(ex));

        executor.execute(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    emitter.send(SseEmitter.event()
                            .id(String.valueOf(i))
                            .name("progress")
                            .data(Map.of("percent", i * 20)));
                    Thread.sleep(500);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter; // returned before sending finishes
    }
}

go deeper

for a junior

Know that SseEmitter streams multiple events over one open HTTP connection using text/event-stream and EventSource; send() then complete().

for a middle

Explain async request processing, the SseEventBuilder fields (id/event/data/retry), and lifecycle callbacks.

for a senior

Discuss threading (drive sends off the request thread), managing an emitter registry, timeouts and disconnect handling.

for a principal

Weigh servlet thread-per-connection scaling limits vs WebFlux, proxy/buffering issues, heartbeats, and backpressure concerns.

**The problem it solves.** A normal Spring MVC controller builds the whole response body, returns it, and the connection closes. That is fine for a JSON document, but not for data that arrives over time — progress updates, live logs, chat tokens, notifications. **Server-Sent Events (SSE)** is a simple, one-directional (server→client) streaming protocol built on a single long-lived HTTP response with Content-Type `text/event-stream`. The browser consumes it with the built-in `EventSource` JavaScript API, which also auto-reconnects. **SseEmitter.** `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` is Spring MVC's handle for producing that stream. It extends `ResponseBodyEmitter` and knows the SSE wire format. Usage pattern: 1. Controller method returns an `SseEmitter` instance (optionally with a timeout, e.g. `new SseEmitter(60_000L)`). 2. Spring detects this return type, switches the request into **async mode** (see `AsyncContext`), sets `Content-Type: text/event-stream`, flushes headers, and **releases the servlet request thread** — the response stays open. 3. Your code (typically on a *different* thread — a scheduler, an `@Async` method, a reactive source, a background task) calls `emitter.send(payload)` for each event. Each call writes and flushes a chunk to the client. 4. When finished, call `emitter.complete()` to end the stream gracefully, or `emitter.completeWithError(throwable)` to end it with an error. **Why another thread?** If you loop-and-send inside the controller method itself you'd block the request thread and defeat the purpose. The idiomatic approach is to return the emitter immediately and drive `send()` from elsewhere, holding a reference (e.g. in a registry/list of active emitters). **The SSE wire format.** Each event is text like: ``` id: 42 event: progress data: {"percent":30} ``` (blank line terminates an event). `SseEmitter.event()` returns an `SseEventBuilder` that lets you set `id(...)`, `name(...)` (the `event:` field), `data(...)`, `reconnectTime(...)` (the `retry:` field), and `comment(...)`. If you just call `emitter.send(obj)` without the builder, Spring emits a plain `data:` line using an `HttpMessageConverter` (e.g. Jackson for JSON) to serialize the object based on the media type. **Completion and cleanup.** SSE connections can be closed by either side. Register callbacks: `emitter.onCompletion(runnable)` (fires when the response finishes for any reason), `emitter.onTimeout(runnable)` (fires when the configured timeout elapses — you should then `complete()`), and `emitter.onError(consumer)` (fires on I/O errors, e.g. the client disconnected). Use these to remove the emitter from your registry so you stop sending to a dead connection. A `send()` to a closed connection throws (often `IOException` / `IllegalStateException`). **Client side.** ```javascript const es = new EventSource('/stream'); es.addEventListener('progress', e => console.log(JSON.parse(e.data))); es.onerror = () => { /* EventSource auto-reconnects */ }; ``` **When to use.** SSE fits server-push, text-based, one-way streams over plain HTTP (works through proxies, no special protocol). For bidirectional/binary realtime use WebSocket instead. For non-SSE incremental output (e.g. streaming a large file or raw bytes) use `StreamingResponseBody` or plain `ResponseBodyEmitter`. **Gotchas.** SSE is text-only (UTF-8); the request thread is freed but the *connection and a container thread for writes* are still consumed per client, so thousands of concurrent SSE clients strain a servlet container (thread-per-connection model) — this is the classic reason people move to WebFlux. Proxies/load balancers may buffer or time out idle streams; heartbeat comments (`:keep-alive`) help.

  • What Content-Type does SseEmitter set, and what browser API consumes it?
    Content-Type text/event-stream. The browser consumes it with the EventSource API, which parses id/event/data fields and auto-reconnects on drop.
  • Why should you not loop and send inside the controller method body directly?
    That would block the request thread for the whole stream, defeating async processing. Return the emitter immediately and drive send() from another thread (executor/@Async/scheduler).

saying these in an interview costs you the question

  • Claiming SSE is bidirectional (it is server→client only; that's WebSocket).
  • Thinking send() must be called before returning the emitter from the controller.
  • Saying you must set Content-Type manually (SseEmitter does it).
  • Confusing SSE with WebSocket protocol.

context