Understanding Tomcat Configuration and Thread Management in Spring Boot 2.7.10

This article explains the default Tomcat settings bundled with Spring Boot 2.7.10, details core parameters such as accept‑count, max‑connections, thread pool sizes, and timeouts, describes the internal Acceptor and Poller threads, provides configuration examples and testing results, and includes reference links.

Top Architect
Top Architect
Top Architect
Understanding Tomcat Configuration and Thread Management in Spring Boot 2.7.10

In Spring Boot 2.7.10 the embedded Tomcat version is 9.0.73, and its default settings are:

Connection waiting queue length (accept‑count): 100

Maximum connections (max‑connections): 8192

Minimum worker threads (min‑spare): 10

Maximum worker threads (max‑threads): 200

Connection timeout: 20 s

The corresponding YAML configuration is:

server:
  tomcat:
    # When all request‑handling threads are busy, the maximum queue length for incoming connections
    accept-count: 100
    # Maximum number of connections the server can accept at any time
    max-connections: 8192
    threads:
      # Minimum number of worker threads created at startup
      min-spare: 10
      # Maximum number of worker threads (IO‑intensive workloads usually need 10×CPU cores)
      max: 200
    # Time (ms) to wait for a request line after a connection is accepted
    connection-timeout: 20000
    # Time (ms) to keep a connection alive when no further request arrives
    keep-alive-timeout: 20000
    # Maximum number of keep‑alive requests before the server closes the connection
    max-keep-alive-requests: 100

Architecture Diagram

When the total number of connections exceeds maxConnections + acceptCount + 1, new requests are not rejected immediately; instead they fail to complete the three‑way TCP handshake and eventually time out after the client’s timeout or Tomcat’s 20 s timeout.

TCP Three‑Way Handshake and Four‑Way Teardown

Sequence Diagram

Core Parameters

AcceptCount

Represents the full connection queue capacity, equivalent to the backlog parameter; it is compared with the Linux somaxconn value and takes the smaller one.

Relevant code (NioEndpoint.java):

serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// bind with accept‑count as backlog
serverSock.socket().bind(addr, getAcceptCount());

MaxConnections

Handled in Acceptor.java:

// Thread run method
public void run() {
    while (!stopCalled) {
        // If we have reached the max connections, wait
        connectionLimitLatch.countUpOrAwait();
        // Accept next incoming connection from server socket
        socket = endpoint.serverSocketAccept();
        // socket.close will trigger connectionLimitLatch.countDown();
    }
}

MinSpareThread / MaxThread

Configured in AbstractEndpoint.java:

// Tomcat startup
public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS, taskqueue, tf);
    taskqueue.setParent((ThreadPoolExecutor) executor);
}

Tomcat extends the JDK thread pool to prioritize thread creation before queuing tasks.

MaxKeepAliveRequests

When the number of keep‑alive requests reaches this value, the server proactively closes the connection. Setting it to 0 or 1 disables keep‑alive; setting it to –1 allows unlimited keep‑alive requests.

NioEndpoint.setSocketOptions
socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());

Http11Processor.service(SocketWrapperBase<?> socketWrapper)
  keepAlive = true;
  while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null &&
         sendfileState == SendfileState.DONE && !protocol.isPaused()) {
    // default 100
    int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests();
    if (maxKeepAliveRequests == 1) {
        keepAlive = false;
    } else if (maxKeepAliveRequests > 0 && socketWrapper.decrementKeepAlive() <= 0) {
        keepAlive = false;
    }
}

ConnectionTimeout

The lifetime of an established connection; if no request arrives within this period, the server closes the connection. In Tomcat 9 the default is 20 000 ms (20 s).

// Check read timeout
if ((socketWrapper.interestOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
    long delta = now - socketWrapper.getLastRead();
    long timeout = socketWrapper.getReadTimeout();
    if (timeout > 0 && delta > timeout) {
        readTimeout = true;
    }
}
// Check write timeout
if (!readTimeout && (socketWrapper.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {
    long delta = now - socketWrapper.getLastWrite();
    long timeout = socketWrapper.getWriteTimeout();
    if (timeout > 0 && delta > timeout) {
        writeTimeout = true;
    }
}

KeepAliveTimeout

Time to wait for another HTTP request before closing the connection. If not set, connectionTimeout is used; a value of –1 disables the timeout.

// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {
    if (keptAlive) {
        // No request data yet, use keep‑alive timeout
        wrapper.setReadTimeout(keepAliveTimeout);
    }
    if (!fill(false)) {
        parsingRequestLinePhase = 1;
        return false;
    }
    // At least one byte of request received, switch to socket timeout
    wrapper.setReadTimeout(connectionTimeout);
}

Internal Threads

Acceptor

The Acceptor receives socket connections, wraps them into NioSocketWrapper, and registers them with the Poller.

public void run() {
    while (!stopCalled) {
        // Wait for next request
        socket = endpoint.serverSocketAccept();
        // Register socket to Poller, generate PollerEvent
        endpoint.setSocketOptions(socket);
        // Add new socket to poller events queue
        poller.register(socketWrapper);
    }
}

Poller

The Poller continuously checks the NIO selector for ready keys, retrieves the associated NioSocketWrapper, and dispatches the request to the thread pool.

public void run() {
    while (true) {
        Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            iterator.remove();
            NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
            if (socketWrapper != null) {
                processKey(sk, socketWrapper);
                executor.execute(new SocketProcessor(socketWrapper, SocketEvent));
            }
        }
    }
}

TomcatThreadPoolExecutor

Tomcat’s custom thread pool extends the JDK ThreadPoolExecutor with a more efficient getSubmittedCount() and a forced‑queue mechanism.

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
    private final AtomicInteger submittedCount = new AtomicInteger(0);
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        if (!(t instanceof StopPooledThreadException)) {
            submittedCount.decrementAndGet();
        }
    }
    @Override
    public void execute(Runnable command) {
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            if (super.getQueue() instanceof TaskQueue) {
                TaskQueue queue = (TaskQueue) super.getQueue();
                try {
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException("threadPoolExecutor.queueFull");
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }
        }
    }
}

Testing

Example configuration for testing different connection limits:

server:
  port: 8080
  tomcat:
    accept-count: 3
    max-connections: 6
    threads:
      min-spare: 2
      max: 3

Use ss -nltp to view the current connection queue size. The article shows results for 6, 9, 10, 11, 12 concurrent connections, illustrating how excess connections remain in SYN_RECV or SYN_SENT state and eventually time out.

If the client sets a timeout, the smaller of the client timeout and the server’s three‑way handshake timeout will determine the final outcome.

References

https://www.zhangbj.com/p/1105.html

https://www.eginnovations.com/blog/tomcat-monitoring-metrics/

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadPoolSpring BootTomcat
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.