4.1 KiB
1. Two protocols, one question
Next: 2. The SSE lifecycle
Server-Sent Events and WebSocket get compared as if they were rivals for the same job. They are not. The question that separates them is much smaller than "which is better", and it is this:
Does the client need to send anything after the first request?
If the answer is no — a dashboard, a progress bar, a notification feed, a log tail, a "your export is ready" ping — SSE is the whole answer, and it is a return type rather than a subsystem. If the answer is yes, and the messages are frequent or need routing between clients, you want WebSocket, and almost certainly STOMP on top of it.
Everything else in the comparison follows from that one asymmetry.
What each one costs you to set up
| SSE | STOMP over WebSocket | |
|---|---|---|
| Dependency | spring-boot-starter-webmvc |
spring-boot-starter-websocket |
| Auto-configuration involved | none | spring-boot-websocket |
| Server-side surface | a handler method returning SseEmitter |
@EnableWebSocketMessageBroker, a configurer, a broker, a converter stack |
| Transport | one long-lived HTTP response | an HTTP/1.1 Upgrade to a framed, bidirectional connection |
| Client | new EventSource(url), built into every browser |
a STOMP library |
| Reconnect | automatic, with Last-Event-ID |
yours to write |
| Server knows who is connected | no | yes — SimpUserRegistry, connect/subscribe/disconnect events |
| Payload | UTF-8 text only | text or binary |
The dependency line is the Boot 4 detail worth stating out loud. spring-boot-starter-websocket
pulls in spring-boot-websocket, which is where the auto-configuration lives. Depending on
org.springframework:spring-websocket and spring-messaging directly compiles and starts, and
then nothing works — the same rule that catches people with spring-kafka in
../kafka-basics and spring-rabbit in
../rabbitmq. In Boot 4, depending on a library rather than on its
Boot starter means you are missing its auto-configuration.
What SSE is not
Two things people assume, both wrong, both measured in this module:
"It pins a thread per client." It does not. SseEmitter switches the request to asynchronous
mode; the request thread goes back to the pool immediately and the response stays open with
nothing attached to it. SseConcurrencyTest
holds forty streams open against a connector whose pool is capped at ten:
emitters open : 40
connector pool size : 10 (max 10)
connector active : 0
"You have to pass a media type or it calls toString()." Also not true, and the opposite is
closer to it. See 3. The payload.
What SSE genuinely cannot do
- Take input.
EventSourceissues one GET and never sends again. Client-to-server traffic has to go over ordinary HTTP requests, which is fine for low rates and awful for a chat. - Send custom headers. The browser's
EventSourcehas no header API, so bearer-token authentication needs a cookie or a query parameter.withCredentialsexists;Authorizationdoes not. - Carry binary. The format is line-oriented UTF-8. Binary means base64, which costs a third.
- Tell you a client left. There is no disconnect event, only a write that fails later — see 2. The SSE lifecycle.
- Escape the browser's per-origin connection limit. Over HTTP/1.1 a browser allows about six concurrent connections to one origin, and an open SSE stream is one of them, per tab. Four tabs of a dashboard is 4 streams; six is a hung site. HTTP/2 multiplexes and the limit effectively disappears, which makes "do we serve this over HTTP/2?" a real part of the decision.
What STOMP costs
A broker, a message-converter stack, a session registry, an origin policy, and a set of routing rules whose defaults are not what you would guess. Chapters 4 and 5 are that list.
Next: 2. The SSE lifecycle