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 offor theX-Accel-Buffering: noresponse header. EventSourcecannot send anAuthorizationheader. Cookie, or a query parameter you then have to keep out of access logs.- Heartbeats. SSE: send a
:comment every N seconds. STOMP:setHeartbeatValueplus aTaskScheduler, and both sides must agree. - Compression.
text/event-streamcompresses extremely well and Boot's HTTP compression is off by default;server.compression.mime-typesmust 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