Why ‘How Much Concurrency Can Spring Boot 3 Handle?’ Is Misleading – A Deep Dive into Max Connections
The article shows that Spring Boot itself does not limit concurrency; the true ceiling is set by Linux TCP parameters, Tomcat's acceptCount and maxConnections, its thread‑pool strategy, and KeepAlive settings, and it walks through source code, default values, and practical experiments to reveal where requests are blocked.
Misconception about Spring Boot concurrency
Stating that Spring Boot can handle only the default 200 Tomcat threads conflates thread count with actual request concurrency and is misleading.
Full stack that determines concurrency
Linux TCP kernel parameters → Tomcat NIO model → connection queue → worker thread pool → Keep‑Alive behavior
Experimental baseline
Tested with:
Spring Boot 2.7.10
Embedded Tomcat 9.0.73
I/O model: NIO
Operating system: Linux
Default Tomcat connector parameters (Spring Boot 2.7.10)
accept-count : 100 – size of the full‑connection backlog
max-connections : 8192 – hard limit of simultaneous connections
min-spare-threads : 10 – initial worker threads
max-threads : 200 – maximum worker threads
connection-timeout : 20 s – timeout while waiting for request data
server:
tomcat:
accept-count: 100
max-connections: 8192
threads:
min-spare: 10
max: 200
connection-timeout: 20000
keep-alive-timeout: 20000
max-keep-alive-requests: 100Behaviour when connections surge
If concurrent connections exceed max-connections + accept-count + 1, Tomcat does not send a TCP RST. The three‑way handshake never completes, and the client times out after roughly 20 seconds. This is a consequence of TCP flow control, not a Tomcat bug.
AcceptCount semantics
In NIO mode Tomcat passes acceptCount to ServerSocketChannel.bind(..., acceptCount) as the backlog. On Linux the effective backlog is min(tomcat.acceptCount, net.core.somaxconn); Windows imposes no system‑level limit.
ServerSocketChannel serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// acceptCount becomes the backlog
serverSock.socket().bind(addr, getAcceptCount());MaxConnections interception point
When the maximum number of connections is reached, the Acceptor thread blocks on a latch before accepting a new socket.
while (!stopCalled) {
// Block when max connections reached
connectionLimitLatch.countUpOrAwait();
socket = endpoint.serverSocketAccept();
// On socket close: connectionLimitLatch.countDown();
}New connections then accumulate in the TCP full‑connection queue.
Tomcat’s thread‑pool strategy vs. standard JDK executor
Tomcat uses a custom TaskQueue that prefers creating new threads over queuing tasks. The overridden offer method returns false (causing the executor to spawn a thread) until the pool reaches maxThreads.
@Override
public boolean offer(Runnable task) {
// If pool size < max, bypass queue and expand threads
if (parent.getPoolSize() < parent.getMaximumPoolSize()) {
return false;
}
// Only queue when threads are at max
return super.offer(task);
}Keep‑Alive impact
Tomcat reads maxKeepAliveRequests from the protocol. When the count is reached, the connection is actively closed.
int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests();
if (maxKeepAliveRequests == 1) {
keepAlive = false;
} else if (maxKeepAliveRequests > 0 && socketWrapper.decrementKeepAlive() <= 0) {
keepAlive = false;
}Setting the value too high keeps connections occupied; setting it too low forces frequent TCP handshakes.
Connection timeout vs. Keep‑Alive timeout
Both default to 20 seconds. If no request data arrives within this window, the connection is closed.
if (timeout > 0 && delta > timeout) {
readTimeout = true;
}Acceptor / Poller / Executor three‑stage model
Accept socket and wrap as NioSocketWrapper.
Register the wrapper with NioEndpoint.Poller.
Submit a SocketProcessor to the executor for request processing.
processSocket(socketWrapper, SocketEvent.OPEN_READ);
executor.execute(new SocketProcessor(...));Extreme test configuration
server:
port: 8080
tomcat:
accept-count: 3
max-connections: 6
threads:
min-spare: 2
max: 3Linux verification (recommended)
Run ss -nlt | grep 8080 to inspect:
Recv‑Q : current queue length (connections waiting for a worker thread)
Send‑Q : capacity of the accept‑count backlog
When connections exceed max-connections + accept-count, the server remains in SYN_RECV (half‑open) while the client stays in SYN_SENT. After ~20 seconds the client times out.
Conclusion
The concurrency ceiling of a Spring Boot application is not set by Spring Boot itself. It is governed by five layers:
Linux TCP parameter somaxconn Tomcat accept-count (full‑connection queue length) max-connections (hard connection limit)
Tomcat’s extended thread‑pool strategy that prefers thread creation over queuing
Keep‑Alive and timeout policies
Thus, assessing “how much concurrency Spring Boot can handle” is equivalent to identifying the shortest weak link among the TCP stack, Tomcat configuration, thread‑pool sizing, and application latency.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
