skip to content

SSE & Streaming Responses

Returning a Flux with text/event-stream or newline-delimited JSON pushes elements to the client as they are produced, with backpressure intact. The natural WebFlux answer to any live-feed design question.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

For an infinite Flux served as SSE, how does Spring WebFlux flush items incrementally, and how does backpressure prevent a fast producer from overwhelming a slow client?

level: seniorimportance: must knowfreq 40%

basics

~20 s

The reactive stack writes and flushes each emitted item as bytes become sendable, keeping the connection open. Backpressure flows from the TCP write buffer up through Reactor: the encoder only requests the next item when the socket can accept more, so a slow client naturally slows the producer.

open as a page

When and why would you return Flux<ServerSentEvent<T>> instead of a plain Flux<T> for an SSE endpoint?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use Flux<ServerSentEvent<T>> when you need to set SSE metadata: the event name, an id (so clients can resume after reconnecting), the retry/reconnect delay, or comment lines. A plain Flux<T> only gives you the data field.

open as a page

Compare application/x-ndjson streaming with text/event-stream (SSE) in WebFlux. When would you choose each?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Both stream a Flux incrementally. NDJSON (application/x-ndjson) sends each item as one JSON object per line — a plain data stream, easy for API/service-to-service clients. SSE (text/event-stream) adds event names, ids, retry, and browser EventSource support.

open as a page

What production concerns arise when running long-lived SSE endpoints at scale in Spring WebFlux, and how do you address them?

level: principalimportance: should knowfreq 30%

basics

~20 s

Long-lived connections tie up sockets and cross proxies/load balancers that may buffer or time out idle streams. Address with heartbeats, sensible timeouts, disconnect cleanup (doOnCancel), non-blocking pipelines, resumption via Last-Event-ID, and disabling proxy response buffering.

open as a page