1
0

Add the sse-websocket module

This commit is contained in:
2026-09-04 00:52:18 +05:30
parent e1f8aa7402
commit 5224afdad2
51 changed files with 2761 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# 6. Choosing, and when the answer is neither
Previous: [5. The limits](05-limits.md)
---
## 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`](../src/main/java/com/ankurm/ssews/EmitterRegistry.java) 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`](output/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](04-stomp.md) 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](05-limits.md)