1
0
Files
spring-messaging-demo/sse-websocket/docs/01-two-protocols.md
2026-09-04 00:52:18 +05:30

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. EventSource issues 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 EventSource has no header API, so bearer-token authentication needs a cookie or a query parameter. withCredentials exists; Authorization does 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