When would you choose the raw low-level WebSocket API over STOMP, and what do you give up?
answer
- raw = bare frames, full control, custom protocol
- STOMP = destinations, pub/sub, @MessageMapping, broker relay
- raw: you rebuild routing + broadcast + session registry
- scale-out: raw is node-local, STOMP+broker fans out
- both need sticky sessions
basics
~20 sUse the raw API when you need a custom or minimal protocol, tight control over frames, or interop with a non-STOMP client. You give up STOMP's built-in destinations, pub/sub broadcast, subscriptions, @MessageMapping routing, acks, and broker integration — you'd have to build session tracking and fan-out yourself.
solid answer
~50 sThe raw `@EnableWebSocket` API gives you bare frames and full control: no imposed subprotocol, minimal overhead, and freedom to define any wire format — ideal for a custom protocol, a very simple notification/echo channel, or interoperating with clients that don't speak STOMP. The cost is that you rebuild everything STOMP gives for free: **destinations and subscriptions**, **broadcast/pub-sub**, `@MessageMapping`/`@SubscribeMapping` routing, message acknowledgements, user-targeted messaging (`convertAndSendToUser`), and **external broker relay** (RabbitMQ/ActiveMQ) that offloads fan-out and enables multi-node scale-out. With raw WebSocket you manually maintain a session registry, write your own routing and broadcast loops, and you must add `ConcurrentWebSocketSessionDecorator` yourself for thread-safe sends. Scaling across nodes is also on you — raw sessions are node-local, so you'd need your own Redis/Kafka fan-out, whereas STOMP+broker handles cross-node delivery. Rule of thumb: reach for STOMP when you need pub/sub semantics and horizontal scale; reach for raw when the protocol is simple or bespoke and you want to own it.
code
java · 16 lines// STOMP gives broadcast for free:
@MessageMapping("/chat")
@SendTo("/topic/room")
public Outbound onMessage(Inbound in) { return process(in); }
// server-initiated: simpMessagingTemplate.convertAndSend("/topic/room", msg);
// Raw equivalent: you own the registry AND the fan-out loop:
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws IOException {
Outbound out = process(parse(message.getPayload()));
TextMessage frame = new TextMessage(serialize(out));
for (WebSocketSession s : sessions.values()) { // manual broadcast
if (s.isOpen()) s.sendMessage(frame); // and only node-local sessions!
}
}go deeper
Know raw is lower-level and STOMP adds destinations/pub-sub; STOMP is easier for chat-like apps.
List concrete STOMP features you'd lose (topics, @MessageMapping, broadcast) and that raw needs manual session tracking.
Weigh custom-protocol/interop needs against rebuilding routing and thread-safe sends; know STOMP applies the concurrent decorator for you.
Center the decision on cross-node fan-out and broker scale-out, operational surface, protocol ownership, and migration risk — not micro-performance.
## Two layers, one transport Both sit on the same WebSocket transport; the difference is the **application protocol** on top. - **Raw (`@EnableWebSocket` + `WebSocketHandler`)**: you receive `TextMessage`/`BinaryMessage` frames and do whatever you want. No routing, no destinations, no broker. - **STOMP (`@EnableWebSocketMessageBroker`)**: STOMP is a simple text framing protocol (SEND/SUBSCRIBE/MESSAGE) that gives you **destinations** (`/topic/...`, `/queue/...`, `/app/...`), controller routing via `@MessageMapping`, a `SimpMessagingTemplate` for server-initiated sends, per-user messaging, and an optional relay to a real broker. ## What raw buys you - **Minimal overhead / custom protocol**: define your own compact binary or text format; no STOMP headers. - **Full control**: exact framing, custom handshake/subprotocol negotiation, streaming via partial messages. - **Interop**: talk to clients/devices that already speak a fixed non-STOMP protocol (IoT, game protocols). - **Simplicity for trivial cases**: a one-way server→client notification stream may not need a broker at all. ## What you give up (and must rebuild) - **Pub/sub & broadcast**: STOMP's `/topic` broadcast becomes a manual loop over a `Map<id, WebSocketSession>`. - **Subscriptions/destinations**: no concept of subscribing to a topic — you route messages yourself. - **Routing**: no `@MessageMapping`; you parse payloads and dispatch by hand. - **User-targeted delivery**: no `convertAndSendToUser`; you maintain your own user→sessions index. - **Broker relay & scale-out**: STOMP can relay to RabbitMQ/ActiveMQ, which fans out across app nodes and survives restarts. Raw sessions are **node-local**; to scale horizontally you must add your own cross-node bus (Redis pub/sub, Kafka) to reach sessions on other instances. - **Backpressure/threading helpers**: STOMP auto-applies `ConcurrentWebSocketSessionDecorator` and exposes send limits; in raw you wire that yourself. - **Acks/receipts, heartbeats**: STOMP defines heartbeats and receipts; raw you implement ping/pong and app-level acks. ## Scaling considerations (the principal-level point) WebSocket connections are **stateful and sticky** — a client stays pinned to one node for the connection's life, so you need sticky load balancing regardless of layer. The divergence is fan-out: broadcasting to all users of a topic means reaching sessions spread across N nodes. STOMP + external broker solves this centrally. With raw WebSocket you either keep it single-node (doesn't scale) or build a message bus yourself. This build-vs-buy calculus, plus protocol ownership and operational surface, is the real decision — not micro-performance. ## Decision guide - **Choose raw** when: the protocol is bespoke/simple, you need minimal overhead or exact frame control, you're bridging a non-STOMP client, or it's a single-node internal tool. - **Choose STOMP** when: you need topics/queues, broadcast, user messaging, an external broker, and multi-node scale — i.e. most collaborative/chat/live-dashboard apps. ## Gotchas - Underestimating the amount of infrastructure you reimplement on raw (routing, fan-out, cross-node) — teams often start raw and migrate to STOMP. - Forgetting stickiness/affinity for either layer. - Assuming raw is 'faster' enough to justify losing broker scale — usually the bottleneck is fan-out, not framing. - Mixing concerns: putting broker-like logic in a singleton handler with unbounded in-memory maps.
- How does horizontal scaling differ between raw WebSocket and STOMP with an external broker?Raw sessions live only on the node that owns the connection, so a broadcast on node A can't reach a client connected to node B unless you build your own cross-node bus (Redis/Kafka). STOMP with a broker relay (RabbitMQ/ActiveMQ) centralizes fan-out, so any node publishes to the broker and it delivers to sessions on all nodes. Both still require sticky load balancing per connection.
- You started with raw WebSocket for a chat feature and now need topic subscriptions and multi-node broadcast. What do you do?Migrate to STOMP (@EnableWebSocketMessageBroker) with an external broker relay rather than reimplementing subscriptions, user routing, and cross-node fan-out on raw handlers. STOMP provides destinations, @MessageMapping, convertAndSendToUser, and broker-backed scale-out out of the box, which is exactly the infrastructure you'd otherwise hand-build.
saying these in an interview costs you the question
- Claiming raw WebSocket gives broadcast/pub-sub out of the box
- Assuming raw scales across nodes without extra infrastructure
- Choosing raw purely for 'performance' while ignoring the fan-out/broker cost
- Not realizing STOMP auto-handles concurrent-send decoration that raw requires manually