skip to content

Is WebSocketSession.sendMessage thread-safe, and how do you safely send to a session from multiple threads?

level: seniorimportance: should knowfreq 35%

answer

  1. sendMessage NOT thread-safe (no interleaved fragments)
  2. ConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit)
  3. serializes sends + backpressure
  4. overflow -> close SESSION_NOT_RELIABLE (or DROP strategy)
  5. STOMP does it for you; raw = do it yourself

basics

~10 s

No. 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
java
@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

for a junior

Just know that sending from many threads to one session is unsafe and Spring has a decorator for it.

for a middle

Name ConcurrentWebSocketSessionDecorator and that inbound echoes are already serialized.

for a senior

Explain frame-interleaving, the constructor limits, and the slow-consumer close semantics; wrap once at connect.

for a principal

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

context