Taming the 8KB Limit: Fixing silent WebSocket disconnects in Spring Boot & Tomcat

Published on: December 29, 2025Spring-Boot Web-Socket

A deep dive into why standard Spring Boot WebSocket configurations fail under large STOMP payloads, and how to bypass the hidden 8KB Tomcat buffer limit using a WebServerFactoryCustomizer.

The Debugging Journey: While building high-throughput messaging systems using STOMP (Simple Text Oriented Messaging Protocol), I identified a critical "leaky abstraction" where large payloads (~70KB) caused silent CloseStatus[code=1009] disconnections. This led to the creation of Spring Boot Issue #47944 and a dedicated reproduction suite for the Spring framework team.

In the world of real-time AI and multimodal streaming, STOMP is often the protocol of choice due to its standardized messaging patterns over WebSockets. However, when these text-based payloads exceed the default 8KB buffer of the embedded Tomcat WsServerContainer, the transport layer abruptly terminates the connection before the application logic can even process the error. When a WebSocket connection drops without a single line in your application logs, you are essentially "chasing ghosts." The transport layer (Tomcat) is terminating the socket at the TCP/buffer level before the Spring Framework even knows a message has arrived.


WebSocket Spring Boot

Let's say you’ve built a real-time app with Spring Boot and WebSockets. Everything works — chat messages fly back and forth, updates appear instantly, and all feels right with the world.
Then, you send your first real payload — a Base64-encoded audio clip, maybe 70 KB. Suddenly… silence. No error in the browser console except a polite reconnect. No ERROR in your Spring Boot logs.

The client (browser) thinks the server (Spring Boot) disappeared, while the server has no idea anything happened.

The Setup: “It Should Be Simple, Right?”

The architectural goal was straightforward: Capture real-time audio in the browser, encode it for transport, and stream it to a Spring Boot backend via a STOMP WebSocket. This is a standard pattern for modern multimodal AI applications where low-latency interaction is key.

Real-time AI Assistant:
I encountered this 8KB bottleneck while building a real-time AI assistant. If you are scaling your WebSocket implementation for AI, you might also run into session persistence issues. See how I solved that here Google AI Agent SDK & Firestore Guide.

The Client-Side: Browser to Socket

On the frontend, we use the MediaRecorder API to capture audio chunks. To send this over a text-based STOMP frame, we encode the binary blob into a Base64 string.


// app.js - Capturing and Publishing Audio
const reader = new FileReader();

reader.onload = () => {
  // Extract the Base64 string from the Data URL
  const base64Audio = reader.result.split(',')[1];
  
  const payload = {
    audioData: base64Audio,
    mimeType: recordedAudioBlob.type // e.g., 'audio/webm'
  };

  // For a typical 1-second audio clip, this payload is ~70KB
  console.log(`Publishing to /app/audio. Size: ${payload.audioData.length} bytes.`);
  
  stompClient.publish({
    destination: "/app/audio",
    body: JSON.stringify(payload)
  });
};

reader.readAsDataURL(recordedAudioBlob);

Server-side handler:


@MessageMapping("/audio")
public void handleAudioMessage(AudioMessage audioMessage) {
    logger.info("Received audio message!");
    // ... process the audio
}

Investigation & Elimination

Checkpoint 1: Message Size Limits
First suspect — message size. Easy fix, right?


        # application.properties
        server.tomcat.websocket.max-text-message-buffer-size=10485760   # 10MB
        spring.websocket.messaging.stomp.message-size-limit=10485760     # 10MB
and in WebConfig.java

    @Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
    registration.setMessageSizeLimit(10 * 1024 * 1024);
    registration.setSendBufferSizeLimit(10 * 1024 * 1024);
}
Restarted the server, result ❌ Same silent disconnect.

Checkpoint 2: Spring Security Configuration
Secondary hypothesis involved Spring Security blocking the request. Disabling CSRF and CORS provided no resolution. Permitted all requests to /ws/** Disabled CSRF Even removed spring-boot-starter-security entirely Restarted the server, result ❌ Same silent disconnect.

The Breakthrough: Enabling Observability

When the framework is silent, you must force it to speak. Enabling DEBUG logging for the org.springframework.web.socket package revealed the underlying error code which was previously swallowed:


        logging.level.org.springframework.web.socket=DEBUG
        logging.level.org.springframework.messaging.simp=DEBUG
            
When the size of the message exceeds the buffer size, the connection is closed with a status code of 1009. o.s.w.s.h.LoggingWebSocketHandlerDecorator : StandardWebSocketSession[...] closed with CloseStatus[code=1009, reason=The decoded text message was too big for the output buffer and the endpoint does not support partial messages]
Enabling `DEBUG` logging for the `org.springframework.web.socket` package revealed the underlying error code which was previously swallowed:

CloseStatus[code=1009, reason=The decoded text message was too big for the output buffer...]
The "Hidden" 8KB Limit: The root cause is embedded Tomcat's default WebSocket message buffer size, which is set to a low 8192 bytes (8KB).

This default is defined in Tomcat's internal Constants.java and, crucially, there are no standard Spring Boot properties to override it.

The Workaround: Customizing the Container

The effective resolution required bypassing high-level Spring configuration and directly customizing the embedded Tomcat container. Specifically, the WsServerContainer needed explicit configuration to increase setMaxTextMessageBufferSize and setMaxBinaryMessageBufferSize beyond the default 8KB.

@Bean
public WebServerFactoryCustomizer tomcatCustomizer() {
    return factory -> factory.addContextCustomizers(context -> 
        context.addServletContainerInitializer((c, ctx) -> {
            ctx.setAttribute(WsServerContainer.class.getName(), new WsServerContainer(ctx) {
                {
                    setDefaultMaxTextMessageBufferSize(512 * 1024); // 512KB
                    setDefaultMaxBinaryMessageBufferSize(512 * 1024);
                }
            });
        }, null));
}

Update: Official Framework Recognition & Resolution

After submitting a GitHub issue 47944 , the Spring team acknowledged the documentation gap and provided a resolution. Andy Wilkinson from the core team has since opened Spring Boot Issue #47951 to address the documentation gap and that issue has been closed.

The Immediate Fix (via Servlet Context):

The team updated the documentation to show that you can currently override these buffers using servlet context parameters in your application.properties:


server.servlet.context-parameters.org.apache.tomcat.websocket.binaryBufferSize=512000
server.servlet.context-parameters.org.apache.tomcat.websocket.textBufferSize=512000
        

The Long-term/future Fix (via Custom Configuration):

Spring-Boot framework has first class dedicated properties for the websocket buffer sizes in WsServerContainer like below which seems to be the spring-boot way. For that we are waiting for peding design review and approval.

server.tomcat.websocket.max-binary-message-buffer-size=512KB
server.tomcat.websocket.max-text-message-buffer-size=512KB
        

Curious to Reproduce the Issue?

I have created a sample project that demonstrates this problem and includes a toggleable workaround.

  • Repository: tts-services
  • Steps:
    • Clone the repo and navigate to the project directory.
    • Set app.websocket.workaround.enabled=false in application.properties.
    • Run the app and connect via http://localhost:8080.
    • Send a 5-10 second audio recording.
    • Observe the CloseStatus[code=1009] in the server logs.

Conclusion:

Modern frameworks like Spring Boot provide excellent abstractions, but they sit atop deep technology stacks (like Tomcat). When high-level configurations strictly fail to resolve resource limit issues, it is often necessary to inspect and configure the underlying container's defaults.

Ready to build more with WebSockets?
Now that your transport layer is stable, take it to the next level by implementing real-time voice and video. Check out my deep dive on Building Multimodal (audio/video) experiences with the Gemini Live API.