skip to content

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

level: seniorimportance: should knowfreq 45%

answer

  1. NDJSON = one JSON per line + \n, no envelope
  2. APPLICATION_NDJSON_VALUE (stream+json deprecated)
  3. SSE = framing + event/id/retry + EventSource
  4. NDJSON for service-to-service; SSE for browsers
  5. both need produces, both backpressured

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.

solid answer

~40 s

Both media types make WebFlux flush a `Flux<T>` item-by-item instead of buffering a JSON array — you switch by setting `produces`. **NDJSON** (`MediaType.APPLICATION_NDJSON_VALUE`, `application/x-ndjson`) writes each element as a standalone JSON document terminated by a newline: `{"id":1}\n{"id":2}\n`. It's pure data with no envelope, ideal for machine-to-machine streaming, bulk exports, and non-browser reactive clients (`WebClient` decodes it straight back into a `Flux<T>`). **SSE** (`text/event-stream`) adds a framing layer — `data:`, `event:`, `id:`, `retry:`, comments — plus native browser support via `EventSource` and automatic reconnection with `Last-Event-ID`. Choose SSE for browser-facing live UIs and resumable feeds; choose NDJSON for backend-to-backend streaming, large result sets, or when the consumer is a reactive client that just wants typed objects with minimal overhead.

code

java · 15 lines
java
// Same data source, two streaming formats chosen by Accept header.
@GetMapping(path = "/prices", produces = {
        MediaType.TEXT_EVENT_STREAM_VALUE,      // browsers / EventSource
        MediaType.APPLICATION_NDJSON_VALUE      // backend / WebClient
})
public Flux<PriceDto> prices() {
    return priceService.stream();  // flushed item-by-item either way
}

// Consuming NDJSON from another service with WebClient:
Flux<PriceDto> live = webClient.get()
    .uri("/prices")
    .accept(MediaType.APPLICATION_NDJSON)
    .retrieve()
    .bodyToFlux(PriceDto.class);  // each line -> one typed object

go deeper

for a junior

Know both stream a Flux and differ by produces media type.

for a middle

Describe the NDJSON wire format and WebClient.bodyToFlux consumption.

for a senior

Give the decision matrix (browser vs backend, framing vs raw) and note the deprecated stream+json.

for a principal

Reason about heartbeats/resumption trade-offs and offering both via content negotiation, and when to escalate to WebSocket/RSocket/broker.

Both are **incremental streaming** formats over one long-lived HTTP response; the choice is about framing and audience. **NDJSON — Newline-Delimited JSON.** - Media type constant: **`MediaType.APPLICATION_NDJSON_VALUE`** = `"application/x-ndjson"`. (An older constant, `APPLICATION_STREAM_JSON_VALUE` / `application/stream+json`, is **deprecated** — don't use it.) - Wire format: one complete JSON value per line, each terminated by `\n`: ``` {"symbol":"ACME","price":100} {"symbol":"ACME","price":101} ``` - Spring uses the **`Jackson2JsonEncoder`** in streaming mode: it serializes each Flux element and flushes it followed by a newline delimiter. No array brackets, no commas between items. - Consuming side: `WebClient` does `.bodyToFlux(PriceDto.class)` and gets a `Flux<PriceDto>` back — each line decoded to a typed object. Command-line tools like `jq` also parse NDJSON line-by-line. - Pure payload: no place for event names, ids, or retry hints. If the connection drops, resumption is entirely up to your own protocol (e.g., a query param cursor). **SSE — Server-Sent Events.** - Media type: **`MediaType.TEXT_EVENT_STREAM_VALUE`** = `"text/event-stream"`. - Richer framing (`data:`/`event:`/`id:`/`retry:`/comments), a defined reconnection protocol (`Last-Event-ID`), and first-class **browser** support via `EventSource`. - Slightly more overhead per event (the `data: ` prefix and blank-line terminator) and UTF-8-text-only. **Decision guide:** | Need | Pick | |---|---| | Browser live UI, auto-reconnect | SSE | | Named channels / resumable feed | SSE | | Backend-to-backend / reactive WebClient consumer | NDJSON | | Bulk export of a large result set | NDJSON | | Minimal per-item overhead, pure typed objects | NDJSON | | Progress + heartbeats to a web page | SSE | **Common ground / gotchas.** - Both REQUIRE the right `produces`; omit it and you get a buffered JSON array (no streaming). - Both honor **backpressure** through Reactor — the encoder only pulls the next item when the network is ready to accept bytes. - Neither is bidirectional; for two-way use WebSocket or RSocket. - NDJSON has no standard heartbeat; SSE uses comment lines. For NDJSON keep-alives you'd emit a sentinel object or rely on TCP keep-alive. - Content negotiation: a single controller method can offer both by declaring `produces = {TEXT_EVENT_STREAM_VALUE, APPLICATION_NDJSON_VALUE}` and letting the client's `Accept` header choose.

  • Your consumer is another Spring service using WebClient. Which format is simpler and why?
    NDJSON. WebClient calls .bodyToFlux(Dto.class) and gets a Flux of typed objects directly, with no SSE envelope to strip. SSE would require decoding data: frames or using the ServerSentEvent type. NDJSON is the lower-overhead, machine-to-machine choice.
  • Which older streaming-JSON media type is deprecated, and what replaced it?
    application/stream+json (MediaType.APPLICATION_STREAM_JSON_VALUE) is deprecated. The current newline-delimited JSON type is application/x-ndjson (MediaType.APPLICATION_NDJSON_VALUE).

saying these in an interview costs you the question

  • Claiming NDJSON supports event names or Last-Event-ID resumption
  • Saying SSE is more efficient than NDJSON for service-to-service
  • Recommending application/stream+json (deprecated)
  • Thinking only one format can stream and the other buffers

context