Skip to main content

Tomcat Thread Pool Tuning: maxThreads and Executor

Tomcat's thread pool is the core resource for handling concurrent requests. Too few threads and requests pile up in the queue; too many and context-switching and memory pressure become problems. Here is how to find the right value for your service.


Tomcat Request Processing Model​

Request arrives
β”‚
β–Ό
[Accept Queue] ← OS socket backlog (acceptCount)
β”‚ if queue full β†’ TCP connection rejected
β–Ό
[Thread Pool] ← maxThreads, minSpareThreads
β”‚ if no idle thread β†’ wait in acceptCount queue
β–Ό
[Servlet Processing]
β”‚
β–Ό
Response returned

Basic Thread Pool Settings (server.xml)​

<Connector port="8080"
protocol="HTTP/1.1"

maxThreads="200"
minSpareThreads="10"
acceptCount="100"
maxConnections="10000"
connectionTimeout="20000"
maxParameterCount="1000"
maxHttpHeaderSize="8192"
/>

Shared Executor for Multiple Connectors​

Share a single thread pool across multiple Connectors (HTTP, HTTPS, AJP).

<!-- 1. Define the Executor -->
<Executor name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="400"
minSpareThreads="10"
maxIdleTime="60000"
maxQueueSize="100"
daemon="true"
threadPriority="5"
/>

<!-- 2. Reference it from Connectors -->
<Connector port="8080"
protocol="HTTP/1.1"
executor="tomcatThreadPool"
acceptCount="100"
maxConnections="10000"
connectionTimeout="20000"
/>

<Connector port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
executor="tomcatThreadPool"
SSLEnabled="true"
scheme="https"
secure="true"
/>

NIO vs NIO2 Protocol​

<!-- HTTP/1.1 NIO (recommended, Java NIO-based) -->
<Connector protocol="HTTP/1.1" .../>

<!-- HTTP/1.1 NIO2 (async I/O, Java 7+) -->
<Connector protocol="org.apache.coyote.http11.Http11Nio2Protocol" .../>

<!-- HTTP/2 Upgrade -->
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/>

Spring Boot Embedded Tomcat Tuning​

# application.yml
server:
tomcat:
threads:
max: 200
min-spare: 10
accept-count: 100
max-connections: 10000
connection-timeout: 20s
max-http-form-post-size: 10MB
@Configuration
public class TomcatConfig {

@Bean
public TomcatServletWebServerFactory tomcatFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractHttp11Protocol<?> http11) {
http11.setMaxThreads(400);
http11.setMinSpareThreads(20);
http11.setAcceptCount(100);
http11.setConnectionTimeout(20000);
}
});

return factory;
}
}

Calculating the Optimal Thread Count​

CPU-bound service (image processing, encryption):
maxThreads β‰ˆ CPU cores Γ— 1–2
e.g., 8 cores β†’ maxThreads = 8–16

I/O-bound service (DB queries, external API calls):
maxThreads β‰ˆ CPU cores Γ— (1 + avg wait / avg compute)
e.g., 8 cores, 200 ms response (180 ms I/O, 20 ms compute):
= 8 Γ— (1 + 180/20) = 8 Γ— 10 = 80–100

Mixed workload (typical web app):
maxThreads = 100–200 (general recommendation)
maxThreads = 200–400 (high-traffic, shared Executor)

Thread State Monitoring​

# Thread dump (shows state of each thread)
kill -3 $(pgrep -f tomcat) # SIGQUIT β†’ written to catalina.out
# Or:
jstack <PID> | grep -E "catalina-exec|WAITING|BLOCKED" | head -50

# Count busy threads
jstack <PID> | grep "catalina-exec" | grep -v "WAITING" | wc -l

# Spring Boot Actuator metrics
curl http://localhost:8080/actuator/metrics/tomcat.threads.busy
curl http://localhost:8080/actuator/metrics/tomcat.threads.current
curl http://localhost:8080/actuator/metrics/tomcat.connections.current
# application.yml: expose Actuator endpoints
management:
endpoints:
web:
exposure:
include: health,metrics,threaddump

Detecting and Handling Thread Pool Exhaustion​

# Symptoms: rising latency, 503 errors, growing queue depth
# Causes: maxThreads exceeded, slow DB queries, missing external API timeouts

# Diagnose β€” check busy threads vs maxThreads
curl http://localhost:8080/actuator/metrics/tomcat.threads.busy

# Diagnose β€” which threads are stuck
jstack <PID> | awk '/catalina-exec/{p=1} p{print; if (/^$/) {p=0}}' | head -100

# Short-term fix: increase maxThreads
# Long-term fix: optimize slow queries, add timeouts for external APIs

<Executor name="tomcatThreadPool"
maxThreads="300"
minSpareThreads="20"
maxIdleTime="60000"
maxQueueSize="100"/>

<Connector port="8080"
protocol="HTTP/1.1"
executor="tomcatThreadPool"
acceptCount="100"
maxConnections="10000"
connectionTimeout="20000"
compression="on"
compressionMinSize="2048"/>