Is WebSocketSession.sendMessage thread-safe, and how do you safely send to a session from multiple threads?
answer
- sendMessage NOT thread-safe (no interleaved fragments)
- ConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit)
- serializes sends + backpressure
- overflow -> close SESSION_NOT_RELIABLE (or DROP strategy)
- STOMP does it for you; raw = do it yourself
basics
~10 sNo. Concurrent sendMessage calls on the same WebSocketSession are not safe and can corrupt the frame stream or throw. Wrap the session in a ConcurrentWebSocketSessionDecorator, which serializes sends and buffers them with size/time limits.
solid answer
~50 s`WebSocketSession.sendMessage` is not thread-safe: the WebSocket spec forbids interleaving fragments of two messages on one connection, so two threads writing to the same session concurrently can produce a corrupt frame stream or an IllegalStateException. Within a single session's inbound callbacks Spring serializes calls, so echoing is fine — the problem is when *external* threads (a scheduler, another user's message, an async job) push to a session. The standard fix is `ConcurrentWebSocketSessionDecorator`, constructed as `new ConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit)`. It queues messages and guarantees only one is written at a time. Crucially it also protects against **slow consumers**: if a client can't keep up, the buffer grows; when it exceeds `bufferSizeLimit` or a send exceeds `sendTimeLimit`, the decorator closes the session with `SESSION_NOT_RELIABLE` instead of letting memory balloon. Wrap sessions in `afterConnectionEstablished` and store the decorator for all outbound sends.
code
java · 25 lines@Component
public class NotificationHandler extends TextWebSocketHandler {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// Serialize sends and cap buffering: 10s send limit, 512KB buffer.
sessions.put(session.getId(),
new ConcurrentWebSocketSessionDecorator(session, 10_000, 512 * 1024));
}
// Called from ANY thread (e.g. a scheduler) - safe because of the decorator.
public void broadcast(String text) throws IOException {
TextMessage msg = new TextMessage(text);
for (WebSocketSession s : sessions.values()) {
if (s.isOpen()) s.sendMessage(msg);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session.getId());
}
}go deeper
Just know that sending from many threads to one session is unsafe and Spring has a decorator for it.
Name ConcurrentWebSocketSessionDecorator and that inbound echoes are already serialized.
Explain frame-interleaving, the constructor limits, and the slow-consumer close semantics; wrap once at connect.
Reason about backpressure strategy (TERMINATE vs DROP), buffer sizing under many slow clients, and how this interacts with fan-out and memory budgets.
## Why sendMessage isn't thread-safe A WebSocket connection carries one logical stream of frames. Large messages may be split into fragments that **must not interleave** with another message's fragments on the same connection (RFC 6455). The underlying servlet/`RemoteEndpoint` write path is also not designed for concurrent callers. So if two application threads call `session.sendMessage(...)` on the *same* `WebSocketSession` at the same time, you can get a corrupted frame sequence on the wire or an `IllegalStateException` like "The remote endpoint was in state ... which is an invalid state for called method". ## When does concurrency actually happen? For **inbound** processing, Spring invokes a session's handler callbacks serially, so replying from within `handleTextMessage` is safe. Concurrency arises when **other** threads send to a session: - A scheduled/`@Async` job pushing notifications. - A broadcast where thread handling user A's message writes to user B's session. - Multiple producer threads fanning out to many sessions. ## ConcurrentWebSocketSessionDecorator Spring provides `org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator`. Constructor: ``` new ConcurrentWebSocketSessionDecorator(WebSocketSession delegate, int sendTimeLimit /* ms */, int bufferSizeLimit /* bytes */) ``` Behavior: - **Serializes sends**: internally queues messages and ensures only one thread writes at a time, so callers never interleave frames. - **Guards against slow clients (backpressure)**: outbound messages queue when the client is slow. If the queued bytes exceed `bufferSizeLimit`, or a single send takes longer than `sendTimeLimit`, the decorator gives up and closes the session with `CloseStatus.SESSION_NOT_RELIABLE` (1006-style). This prevents an unbounded in-memory queue and OOM from one stuck consumer. There is also an overload accepting an `OverflowStrategy` (`TERMINATE` — the default behavior above, or `DROP` — discard oldest queued messages instead of closing). ## Usage pattern Decorate once when the connection opens and use the decorator everywhere you send: ``` public void afterConnectionEstablished(WebSocketSession session) { WebSocketSession concurrent = new ConcurrentWebSocketSessionDecorator(session, 10_000, 512 * 1024); sessions.put(session.getId(), concurrent); } ``` Note: STOMP messaging (`@EnableWebSocketMessageBroker`) already applies this decorator under the hood via `sendTimeLimit`/`sendBufferSizeLimit` on the transport registration. In the **raw** API you must apply it yourself. ## Gotchas - Wrapping the session but still holding a reference to the raw one and sending through that defeats the purpose — always send through the decorator. - Setting `bufferSizeLimit` too low drops otherwise-healthy clients on bursts; too high risks memory pressure under many slow clients. Tune to message size × acceptable backlog. - The decorator serializes but does not make *reads* concurrent-safe; it's purely about outbound sends. - Slow-consumer close appears as an abnormal close on the client — clients should reconnect.
- What happens when a client is too slow to drain the send buffer?Queued outbound messages accumulate in the decorator. Once they exceed bufferSizeLimit (or a send exceeds sendTimeLimit), the default TERMINATE strategy closes the session with CloseStatus.SESSION_NOT_RELIABLE, protecting the server from unbounded memory growth. The DROP strategy instead discards the oldest queued messages.
- Do you need this decorator when using STOMP over WebSocket?No — the STOMP messaging layer applies a ConcurrentWebSocketSessionDecorator internally, tunable via setSendTimeLimit/setSendBufferSizeLimit on the transport registration. The manual decorator is specifically for the low-level raw API.
saying these in an interview costs you the question
- Claiming sendMessage is thread-safe because 'the session is one object'
- Broadcasting from multiple threads to the same session without any serialization
- Thinking the decorator only serializes and ignoring its slow-consumer/backpressure role
- Assuming the raw API auto-applies the decorator like STOMP does