skip to content

How do you expose a streaming endpoint in Spring WebFlux, and what does returning a Flux with produces=MediaType.TEXT_EVENT_STREAM_VALUE actually do?

level: juniorimportance: must knowfreq 70%

answer

  1. produces = TEXT_EVENT_STREAM_VALUE
  2. default JSON -> buffers whole array
  3. Flux item -> data: frame
  4. connection stays open, flush per item
  5. one-way server->client, UTF-8 text

basics

~10 s

Return a Flux from the controller and set produces=MediaType.TEXT_EVENT_STREAM_VALUE ("text/event-stream"). Spring keeps the HTTP response open and writes each emitted item to the client as a Server-Sent Event, instead of buffering one JSON array.

solid answer

~40 s

In WebFlux a controller method can return Flux<T>. Normally, with produces=application/json, Spring collects the whole Flux and serializes it as a JSON array. To stream, you set produces=MediaType.TEXT_EVENT_STREAM_VALUE ("text/event-stream"), which switches the reactive HttpMessageWriter to Server-Sent Events: each element the Flux emits is serialized and flushed to the client immediately as a `data:` frame, keeping the connection open. The client (an EventSource in the browser, or a WebClient consuming Flux) receives events as they arrive. You can return Flux<String>, Flux<MyDto> (auto-serialized to JSON in the data field), or Flux<ServerSentEvent<T>> for full control over event name, id, and retry. This is ideal for live feeds, progress updates, or LLM token streaming where you don't want to wait for the whole result.

code

java · 19 lines
java
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.time.Duration;

@RestController
@RequestMapping("/api/stream")
class PriceController {

    // Without produces=text/event-stream Spring would buffer this into a
    // JSON array and never return for an infinite Flux.
    @GetMapping(path = "/prices", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<PriceDto> prices() {
        return Flux.interval(Duration.ofSeconds(1))
                   .map(tick -> new PriceDto("ACME", 100 + tick));
    }
}

record PriceDto(String symbol, long price) {}

go deeper

for a junior

Know the annotation attribute produces=TEXT_EVENT_STREAM_VALUE and that Flux items are flushed one at a time.

for a middle

Explain the JSON-array-buffering default vs the SSE writer switch, and the three return-type options.

for a senior

Discuss EventSource limitations, UTF-8-only constraint, and choosing SSE vs WebSocket.

for a principal

Frame SSE within the non-blocking thread model and when incremental push beats request/response.

**Server-Sent Events (SSE)** is a simple, one-way (server-to-client) streaming protocol layered on a normal HTTP response. The server holds the connection open and pushes text-based events over time. The wire format is line-oriented: each event is one or more lines like `data: ...`, optionally `event: ...`, `id: ...`, `retry: ...`, and an empty line terminates the event. The media type is `text/event-stream`. **Why WebFlux fits SSE.** Spring WebFlux is the reactive, non-blocking web stack built on Project Reactor. A controller returns a **`Flux<T>`** (a reactive stream of 0..N items) or **`Mono<T>`** (0..1). Because the stack is non-blocking, one thread can service many open streaming connections without a thread parked per client (unlike Spring MVC's thread-per-request model). **What `produces` controls.** The `produces` attribute of `@GetMapping`/`@RequestMapping` sets the response `Content-Type` AND selects which reactive `HttpMessageWriter` serializes the return value: - `produces = MediaType.APPLICATION_JSON_VALUE` (the default for a `Flux<Dto>`): Spring uses `Jackson2JsonEncoder`, **collects the entire Flux** and writes it as a single JSON array `[...]`. The client gets nothing until the stream completes. Good for a finite collection, useless for an infinite/live stream. - `produces = MediaType.TEXT_EVENT_STREAM_VALUE` (`"text/event-stream"`): Spring uses the **`ServerSentEventHttpMessageWriter`**. Each emitted item is serialized and flushed as its own SSE frame immediately. The connection stays open until the Flux completes, errors, or the client disconnects. **Return-type options with SSE:** - `Flux<String>` -> each string becomes `data: <string>`. - `Flux<MyDto>` -> each item is JSON-serialized into the `data:` field. - `Flux<ServerSentEvent<MyDto>>` -> you control the full event: `event` (name), `id` (last-event-id for resumption), `retry` (reconnect delay), `comment`, and `data`. **Minimal example:** ```java @GetMapping(path = "/prices", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<PriceDto> prices() { return priceService.streamPrices(); // emits over time } ``` **Gotchas.** - Forgetting `produces` -> Spring buffers into a JSON array; the endpoint appears to "hang" for an infinite Flux and never sends data. - SSE is **UTF-8 text only**; binary data must be base64-encoded. - SSE is one-directional (server->client). For bidirectional, use WebSocket/RSocket. - Browsers' native `EventSource` only does GET and cannot send custom headers (auth cookies work); non-browser clients (WebClient) are more flexible. **When to use.** Live dashboards, notifications, progress bars, log tailing, and token-by-token LLM output — anywhere the server produces data incrementally and the client should render it as it arrives.

  • What happens if you return Flux<PriceDto> WITHOUT produces=text/event-stream?
    Spring uses the JSON encoder, collects the whole Flux, and writes it as one JSON array. For a finite Flux the client waits until completion; for an infinite Flux the response never completes and the client sees nothing.
  • Can the browser's EventSource send an Authorization header?
    No. Native EventSource only issues GET requests and cannot set custom headers; it does send cookies, so cookie-based auth works. For bearer tokens you need a polyfill or a non-browser client like WebClient.

saying these in an interview costs you the question

  • Thinking SSE is bidirectional like WebSocket
  • Claiming you must use Flux<ServerSentEvent> — plain Flux<Dto> works too
  • Believing the default Flux<Dto> return streams incrementally without setting produces
  • Saying SSE can carry raw binary data

context