Understanding Spring Boot 2.7.10’s Embedded Tomcat: Defaults, Thread Pools, and Connection Limits

This article examines Spring Boot 2.7.10’s embedded Tomcat configuration, detailing default connection queue sizes, thread pool parameters, key Tomcat settings such as AcceptCount and MaxConnections, internal thread components, and practical testing results that reveal how connection limits affect client‑server handshakes.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Understanding Spring Boot 2.7.10’s Embedded Tomcat: Defaults, Thread Pools, and Connection Limits

Overview

Spring Boot 2.7.10 bundles Tomcat 9.0.73 as its embedded servlet container. The following sections describe the default Tomcat configuration, the relevant source‑code locations, the internal thread model, and how to verify the settings with a simple test.

Default Tomcat Settings (Spring Boot 2.7.10)

Connection waiting queue length ( accept-count): 100

Maximum connections ( max-connections): 8192

Minimum spare worker threads ( min-spare): 10

Maximum worker threads ( max): 200

Connection timeout ( connection-timeout): 20 000 ms (20 s)

Keep‑alive timeout ( keep-alive-timeout): 20 000 ms (‑1 disables)

Maximum keep‑alive requests ( max-keep-alive-requests): 100

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: 100

Key Tomcat Parameters

AcceptCount

Defines the size of the TCP backlog queue (similar to Linux somaxconn). Tomcat binds the server socket with this value in org.apache.tomcat.util.net.NioEndpoint:

ServerSocketChannel serverSock = ServerSocketChannel.open();
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.socket().bind(addr, getAcceptCount());

MaxConnections

Managed by org.apache.tomcat.util.net.Acceptor. When the limit is reached the acceptor thread blocks on a latch until a slot frees:

while (!stopCalled) {
    connectionLimitLatch.countUpOrAwait();
    Socket socket = endpoint.serverSocketAccept();
    // process socket …
}

MinSpareThread / MaxThread

Created in org.apache.tomcat.util.net.AbstractEndpoint.createExecutor(). Tomcat builds a custom ThreadPoolExecutor with core pool size = minSpareThreads and maximum = maxThreads:

executor = new ThreadPoolExecutor(
    getMinSpareThreads(),
    getMaxThreads(),
    60, TimeUnit.SECONDS,
    taskqueue,
    tf);

MaxKeepAliveRequests

Controls how many HTTP requests a persistent connection may serve. Values:

0 or 1 – disables keep‑alive

-1 – unlimited

int max = protocol.getMaxKeepAliveRequests();
if (max == 1) {
    keepAlive = false;
} else if (max > 0 && socketWrapper.decrementKeepAlive() <= 0) {
    keepAlive = false;
}

ConnectionTimeout

Time Tomcat waits for a request line after a connection is accepted. Default = 20 000 ms. Too short drops legitimate clients; too long reduces responsiveness.

KeepAliveTimeout

Time to wait for the next request on an idle keep‑alive connection. If unset, connectionTimeout is used; -1 disables the timeout.

Internal Thread Model

Acceptor

Continuously accepts new sockets and registers them with the poller.

public void run() {
    while (!stopCalled) {
        Socket socket = endpoint.serverSocketAccept();
        endpoint.setSocketOptions(socket);
        poller.register(socketWrapper);
    }
}

Poller

Uses a NIO Selector to detect ready events, creates SocketProcessor tasks and hands them to the executor.

while (true) {
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    while (it.hasNext()) {
        SelectionKey sk = it.next();
        it.remove();
        NioSocketWrapper sw = (NioSocketWrapper) sk.attachment();
        if (sw != null) {
            processKey(sk, sw);
            executor.execute(new SocketProcessor(sw, SocketEvent.OPEN_READ));
        }
    }
}

TomcatThreadPoolExecutor

Extends java.util.concurrent.ThreadPoolExecutor to track submitted tasks and to provide a custom TaskQueue that can force tasks into the queue when the pool is saturated.

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 q = (TaskQueue) super.getQueue();
                try {
                    if (!q.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException("queueFull");
                    }
                } catch (InterruptedException e) {
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(e);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }
        }
    }
}

TaskQueue (Custom Queue)

The queue overrides offer so that new threads are created up to the maximum pool size before tasks are enqueued. It also provides a force method to bypass capacity checks.

public boolean offer(Runnable r) {
    if (parent == null) return super.offer(r);
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(r);
    if (parent.getSubmittedCount() < parent.getPoolSize()) return super.offer(r);
    if (parent.getPoolSize() < parent.getMaximumPoolSize()) return false; // trigger new thread
    return super.offer(r);
}

public boolean force(Runnable o) {
    if (parent == null || parent.isShutdown()) {
        throw new RejectedExecutionException("taskQueue.notRunning");
    }
    return super.offer(o);
}

Testing the Configuration

Sample application.yml used to observe the effect of a small backlog and thread pool:

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

Run ss -nltp (or netstat -nltp) to view the socket state. Increase concurrent connections and note the behavior:

When total connections exceed max-connections + accept-count, new SYN packets remain in SYN_RECV on the server and SYN_SENT on the client, causing a 20 s handshake timeout.

If the client also sets a timeout, the shorter of the two determines when the connection is aborted.

Clients must align their own connection‑establishment timeout with the server’s 20 s limit to avoid premature failures.

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.

Backend Developmentperformance tuningSpring Bootthread poolTomcatConnection Managementembedded server
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow 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.