Taming the 8KB Limit: Fixing silent WebSocket disconnects in Spring Boot & Tomcat
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.
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.
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
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...]
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 inWsServerContainer 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=falseinapplication.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.