1
0
Files
2026-09-04 00:52:18 +05:30

4.2 KiB

6. Choosing, and when the answer is neither

Previous: 5. The limits


The decision, in one table

If Use Because
Server pushes, client never speaks SSE A return type. No broker, no converter stack, no origin policy. Reconnect is free.
Clients talk to each other, or to the server, often STOMP over WebSocket Routing, presence, per-user destinations, and a path to a real broker without touching controllers.
One-off progress on an operation the client already started SSE The stream is scoped to the request. Nothing to clean up.
Payloads are binary, or large WebSocket SSE is line-oriented UTF-8; binary costs a third in base64.
The consumer is another service, not a browser neither — see below Browsers are the only reason to accept SSE's or STOMP's constraints.
You need the consumer to say "slow down" neither Covered in the next article.

The costs nobody puts in the table

SSE costs a connection per stream, per tab. Over HTTP/1.1 a browser allows about six concurrent connections per origin, and an open EventSource is one of them. Three tabs of your dashboard leaves three for everything else on the page. Over HTTP/2 they multiplex over one connection and the limit effectively disappears — so "are we behind HTTP/2 end to end, including the proxy?" is a real input to this decision, not a detail.

SSE costs one serialisation per subscriber per event. The fan-out in EmitterRegistry.broadcast is a loop: a thousand open dashboards means a thousand send() calls on one scheduler thread, every tick. The obvious fix — serialise once and write the bytes N times — is not something SseEmitter offers.

SSE does not pin threads, though. That objection is wrong, and sse-concurrency.txt is the evidence:

=== server.tomcat.threads.max=10, clients=40 ===
emitters open        : 40
connector pool size  : 10 (max 10)
connector active     : 0
one broadcast reached: 40 streams
clients that read it : 40

STOMP costs a stateful server. Sessions live in one JVM's memory. Two instances behind a load balancer do not share subscriptions, so a message published on instance A never reaches a subscriber on instance B — which is the real reason to move to a broker relay, well before you need persistence.

Should you build this at all?

Three cases where the honest answer is no:

  • Updates arrive every few minutes. Polling every thirty seconds is one endpoint, no lifecycle, no proxy configuration, no reconnect logic and no leaked emitters. The right answer more often than it is chosen.
  • You want a chat and you have one server and no plan for a second. Everything in chapter 4 is real work, and the first time you scale out you will be doing it again with a broker relay. Decide about the broker first.
  • The client is another service. SSE's advantage is that browsers implement it. STOMP's advantage is that browsers can speak it. Neither advantage applies service-to-service, and neither protocol gives you flow control, deadlines or a schema — which is what the companion article on RSocket, gRPC and raw WebSocket is about.

Everything else, one line each

  • Behind nginx, a chunked SSE response is buffered by default and the client sees nothing until the buffer fills. proxy_buffering off or the X-Accel-Buffering: no response header.
  • EventSource cannot send an Authorization header. Cookie, or a query parameter you then have to keep out of access logs.
  • Heartbeats. SSE: send a : comment every N seconds. STOMP: setHeartbeatValue plus a TaskScheduler, and both sides must agree.
  • Compression. text/event-stream compresses extremely well and Boot's HTTP compression is off by default; server.compression.mime-types must list it explicitly.
  • Surefire does not discover static nested test classes. Seven of this module's seventeen tests silently did not run until they were split into top-level classes. Check the count, not the colour.

Previous: 5. The limits