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

sse-websocket — a live dashboard over SSE and a chat over STOMP

Companion project for Server-Sent Events and WebSocket on Spring Boot 4: SseEmitter, STOMP, and Which to Pick on ankurm.com.

Seventeen tests against a real embedded Tomcat, talking over real sockets. No Docker, no broker to install, nothing but a JDK. ./scripts/run-all.sh regenerates everything under docs/output/.

Versions

Version Notes
JDK 25 (Temurin 25.0.4.1+1) current LTS
Spring Boot 4.1.1 latest GA of the 4.1 line
Spring Framework 7.0.9 Boot-managed
Tomcat 11.0.24 Boot-managed; its WebSocket buffer is the limit that matters
Jackson 3.1.5 (tools.jackson) why JacksonJsonMessageConverter, not MappingJackson2MessageConverter

Read from repo1.maven.org/.../maven-metadata.xml and from Boot's own spring-boot-dependencies POM, not from release announcements.

Quickstart

./scripts/run-all.sh          # every test, then regenerate docs/output/
mvn test                      # the same without the capture
mvn spring-boot:run           # then: curl -N localhost:8080/sse/metrics
curl -s localhost:8080/diag   # open streams, counters, converter order, container buffers

Endpoints

Path What it is for
GET /sse/metrics the dashboard stream; SseEmitter(Long.MAX_VALUE), fed by a scheduled broadcast
GET /sse/raw three events and a close, so the wire format can be captured verbatim
GET /sse/payload the same record with and without a media type, plus the same for a String
GET /sse/silent?timeoutMs= one event then silence, to observe an async timeout from the client side
GET /sse/resume echoes Last-Event-ID and replays from it
GET /diag emitter counters, SimpUserRegistry count, converter order, container buffer sizes
GET /diag/async-timeout the effective async timeout, read off the live AsyncContext
WS /ws the STOMP endpoint
WS /ws-sockjs the SockJS fallback — a separate endpoint, not an option on the first

STOMP destinations: SEND /app/chat.send/topic/room; SEND /app/chat.echo/topic/chat.echo (no @SendTo, on purpose); SEND /app/chat.whisper/user/queue/whisper.

Profiles

Profile Effect
(none) Tomcat's own WebSocket buffers — 8 192 bytes
bigframes ServletServerContainerFactoryBean with 256 KB buffers; -Dws.buffer-bytes= to vary it

Documentation

  1. Two protocols, one question
  2. The SSE lifecycle, and the three ways a stream ends
  3. The payload, and a media type that does less than you think
  4. STOMP: four lines of configuration, three surprising defaults
  5. The limits: origin, size, and which one actually fires
  6. Choosing, and when the answer is neither

Captured output

File What it shows
sse-wire-format.txt the raw bytes of an SSE response, headers included
sse-payload-conversion.txt data(x) vs data(x, APPLICATION_JSON) for a record and a String
sse-lifecycle.txt an async timeout as the client sees it, and how many writes a dead client absorbs
async-timeout.txt the container default and the property override, read off AsyncContext
sse-concurrency.txt 40 open streams on a 10-thread connector
stomp-routing.txt fan-out, the default destination, the controller bypass, user destinations, presence
websocket-size-limits.txt the ceiling binary-searched at four container buffer sizes
handshake-origin.txt 101 / 101 / 403 for absent, same and foreign Origin
tests.txt 17 tests

Six things this module exists to prove

  1. SSE does not pin a thread per client. Forty streams stay open on a connector pool capped at ten, with zero active threads.
  2. An unset spring.mvc.async.request-timeout means 30 seconds on Tomcat 11. Read off the live AsyncContext, not quoted. That is why dashboards reconnect every 30 s with nothing in the log.
  3. A dead SSE client is discovered by a failed write, and not by the first one. One full serialise-and-write succeeded into a socket whose client had already gone.
  4. The media type argument to SseEmitter.data(..) is a filter, not a preference. For a record it changes nothing; for a String it changes nothing either, because StringHttpMessageConverter claims the write first.
  5. A STOMP client can publish straight to /topic/** and skip every controller. The timestamp the controller would have overwritten arrives untouched.
  6. Spring's 64 KB messageSizeLimit is never reached on a stock Tomcat. The container's 8 192-byte buffer caps a STOMP frame at about 16 KB and closes the connection with status 1009 — no exception, no log line from Spring.