skip to content

Compare ResponseBodyEmitter, SseEmitter, and StreamingResponseBody. When would you choose each?

level: middleimportance: must knowfreq 50%

answer

  1. Emitter = objects over time via converters
  2. SseEmitter = Emitter + text/event-stream framing
  3. StreamingResponseBody = raw OutputStream bytes
  4. SSE→browser, Emitter→objects, SRB→files
  5. all switch request to async

basics

~20 s

All three stream a response incrementally. ResponseBodyEmitter pushes objects through message converters over time; SseEmitter is a subclass that formats them as Server-Sent Events (text/event-stream). StreamingResponseBody gives you the raw OutputStream to write bytes yourself — ideal for large files or non-object data.

solid answer

~40 s

They are three streaming return types in Spring MVC. ResponseBodyEmitter lets you emit a sequence of Java objects over time; each send() runs through the matching HttpMessageConverter, and you control the media type. SseEmitter extends it and encodes each send as an SSE event with Content-Type text/event-stream, adding the event()/SseEventBuilder helpers for id/event/retry. StreamingResponseBody is different: instead of emitting objects, you get a callback with the raw OutputStream and write bytes directly, so it suits large downloads, CSV/report generation, proxying, or media where you don't want object serialization or buffering the whole payload in memory. Use SseEmitter for browser EventSource push, ResponseBodyEmitter for custom object streams (e.g. JSON streaming to a non-browser client), and StreamingResponseBody for byte/file streaming.

code

java · 30 lines
java
// StreamingResponseBody: stream a large CSV without buffering it all
@GetMapping("/export.csv")
public ResponseEntity<StreamingResponseBody> exportCsv() {
    StreamingResponseBody body = out -> {
        Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8);
        w.write("id,name\n");
        for (int i = 0; i < 100_000; i++) {
            w.write(i + ",row-" + i + "\n");
            if (i % 1000 == 0) w.flush(); // push chunks incrementally
        }
        w.flush();
    };
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=export.csv")
            .contentType(new MediaType("text", "csv"))
            .body(body);
}

// ResponseBodyEmitter: stream serialized objects (non-SSE)
@GetMapping(value = "/objects", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseBodyEmitter streamObjects(TaskExecutor executor) {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    executor.execute(() -> {
        try {
            for (int i = 0; i < 3; i++) emitter.send(new Item(i));
            emitter.complete();
        } catch (IOException e) { emitter.completeWithError(e); }
    });
    return emitter;
}

go deeper

for a junior

Know the three names and that all stream incrementally; SSE is for browser push, StreamingResponseBody for files.

for a middle

Explain the object-vs-bytes distinction, that SseEmitter extends ResponseBodyEmitter, and message-converter involvement.

for a senior

Discuss async executor configuration, ResponseEntity wrapping for headers, and where exceptions surface after commit.

for a principal

Contrast the servlet blocking model with WebFlux Flux backpressure and reason about memory/thread trade-offs at scale.

All three types make Spring MVC hold the response open and switch the request into **async processing** (freeing the servlet request thread) so output can be produced incrementally rather than as one buffered blob. The difference is *what* you emit and *how it's serialized*. **1. `ResponseBodyEmitter`** (`org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter`). The general-purpose emitter of **objects over time**. You return it, then call `emitter.send(object)` (optionally `send(object, mediaType)`); Spring picks a matching `HttpMessageConverter` (e.g. Jackson) to serialize each object and writes it to the response. You choose the overall response media type via the mapping's `produces` or the send overloads. Lifecycle callbacks: `onCompletion`, `onTimeout`, `onError`, plus `complete()` / `completeWithError()`. Use it when you want a *streamed sequence of serialized objects* that is **not** SSE — for example, streaming many JSON objects to a custom client, or a long NDJSON-like feed. **2. `SseEmitter` extends `ResponseBodyEmitter`.** It specializes the general emitter for the **Server-Sent Events** protocol: it forces `Content-Type: text/event-stream`, and each `send()` is wrapped in SSE framing (`data:` lines, blank-line terminators). It adds `SseEmitter.event()` returning an `SseEventBuilder` so you can set the SSE-specific fields: `id(...)`, `name(...)` → `event:`, `reconnectTime(...)` → `retry:`, `comment(...)`, and `data(...)` (which still uses message converters to serialize the payload). Use it when a **browser `EventSource`** (or any SSE client) consumes the stream. **3. `StreamingResponseBody`** (`org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody`). A functional interface: `void writeTo(OutputStream out)`. You (or a lambda) get the **raw servlet OutputStream** and write bytes directly — no message converter, no object model, no per-event framing. Spring runs the callback on an async task thread. Use it when you produce **bytes**: large file/report downloads, generated CSV/PDF, streaming a database ResultSet to CSV, proxying an upstream stream — anything where holding the whole payload in memory is wasteful. Return it wrapped in `ResponseEntity<StreamingResponseBody>` to set headers like `Content-Disposition`. **Choosing:** - Browser push / live events → **SseEmitter**. - Streamed sequence of serialized objects, non-SSE, custom protocol → **ResponseBodyEmitter**. - Raw bytes / big files / your own encoding → **StreamingResponseBody**. **Shared gotchas.** All three consume async infrastructure — configure `spring.mvc.async.request-timeout` or per-instance timeouts; register error/timeout callbacks; and remember the actual writing typically happens on a **task executor thread**, so exceptions thrown there won't hit your `@ControllerAdvice` the same way (the response may already be committed). For `StreamingResponseBody`, configure the async task executor (`WebMvcConfigurer.configureAsyncSupport`) in production so you don't use the default SimpleAsyncTaskExecutor. None of these is reactive/backpressured — they run on the servlet (blocking) model; for true backpressure use WebFlux `Flux`. **Common confusion:** returning `Flux`/`Mono` is WebFlux, not these Spring MVC types (though Spring MVC can adapt a reactive return value via `ReactiveAdapterRegistry`, the three types above are the native servlet-stack streaming APIs).

  • Why choose StreamingResponseBody over SseEmitter for a big file download?
    StreamingResponseBody gives raw OutputStream access to write bytes without SSE framing or object serialization, and doesn't buffer the whole file in memory. SSE is UTF-8 text with event framing — wrong for binary files, and EventSource isn't a download mechanism.
  • Does SseEmitter still use HttpMessageConverters?
    Yes. The data() payload of each SSE event is serialized by a matching converter (e.g. Jackson to JSON); SseEmitter only adds the text/event-stream framing on top.

saying these in an interview costs you the question

  • Saying StreamingResponseBody serializes objects via Jackson (it writes raw bytes, no converter).
  • Thinking SseEmitter and ResponseBodyEmitter are unrelated (SseEmitter extends ResponseBodyEmitter).
  • Claiming these provide reactive backpressure (they're servlet/blocking; that's WebFlux Flux).
  • Using SseEmitter to serve a binary file download.

context