Sharing Tomcat’s Thread Pool Across Connectors to Save Memory and Simplify Tuning
Learn how to configure a shared Tomcat thread pool to cut memory waste and simplify tuning across HTTP and AJP connectors.
21 Aug 2026, 02:32 UTC

The problem: duplicated thread pools waste resources
When you run Tomcat with more than one network connector – for example an HTTP listener on port 8080 and an AJP listener for a front‑end web server – each connector creates its own ThreadPoolExecutor by default. Under variable traffic the two pools are sized independently, so the total number of threads can exceed what the host actually needs. Memory is consumed by idle threads, and tuning becomes a guessing game because you have to adjust each connector separately.
Thesis: a shared <Executor> centralises thread‑pool control
Tomcat 7.0 introduced a global <Executor> element that can be referenced by one or more <Connector> elements. All connectors that point to the same executor draw threads from a single java.util.concurrent.ThreadPoolExecutor. This lets you set maxThreads, minSpareThreads, maxIdleTime and queueSize once, reduces overhead, and makes capacity planning more predictable.
How the shared executor works
The <Executor> element is parsed during startup and creates a ThreadPoolExecutor with the supplied attributes. The connector’s executor attribute tells Tomcat to use that pool instead of building its own. Internally the executor still respects the usual ThreadPoolExecutor behaviour: core threads are created up to minSpareThreads, the queue holds pending tasks when all threads are busy, and extra threads are spawned up to maxThreads when the queue fills.
Worked example: configuring a shared pool for HTTP and AJP
- Locate your instance’s configuration directory, usually
$CATALINA_BASE/conf. - Edit
server.xml(you need read/write permission on the file and the ability to restart Tomcat). - Add the executor definition before the first
<Connector>:
<Executor name='tomcatThreadPool'
namePrefix='catalina-exec-'
maxThreads='200'
minSpareThreads='25'
maxIdleTime='60000'
queueSize='100' />
- Modify each connector to reference the executor and remove its own thread‑pool attributes:
<Connector port='8080' protocol='HTTP/1.1'
executor='tomcatThreadPool'
connectionTimeout='20000'
redirectPort='8443' />
<Connector port='8009' protocol='AJP/1.3'
executor='tomcatThreadPool'
redirectPort='8443' />
After saving the file, restart Tomcat. No other changes are required.
Trade‑off and limitation
The main drawback of a shared pool is loss of isolation: a sudden spike in AJP traffic can consume threads that would otherwise serve HTTP requests, potentially raising latency for web clients. If you need strict QoS guarantees, consider keeping separate executors or using connector‑specific attributes like acceptCount to bound queue growth. Additionally, the APR/native connector uses its own native thread pool; the generic <Executor> is not compatible with it, so you should either disable APR or revert to the connector’s native pooling for optimal performance.
Verifying the setup
- Check the Tomcat version (the feature exists from 7.0 onward). Run:
Look for a line like$CATALINA_BASE/bin/version.shServer version: Apache Tomcat/9.0.XX. - Start Tomcat and scan the console log for a message indicating the executor is being used, e.g.:
INFO: Initializing ProtocolHandler ["http-nio-8080"]
INFO: Initializing ProtocolHandler ["ajp-nio-8009"]
INFO: Starting Service [Catalina]
INFO: Starting Servlet Engine: Apache Tomcat/9.0.XX
INFO: Initializing Executor [tomcatThreadPool]
Alternatively, connect with JConsole (or VisualVM) to the Tomcat process, navigate to the MBeans tab, find java.util.concurrent.ThreadPoolExecutor instances, and verify that the pool named catalina-exec- shows the maxThreads you set (200 in the example).
wrk (you need network access and the ability to generate traffic):
wrk -t12 -c200 -d30s http://localhost:8080/
While the test runs, monitor the executor’s active thread count via JConsole; it should stay ≤ 200.Actionable closing
If you operate Tomcat with multiple connectors and notice idle threads or difficult tuning, try defining a shared <Executor> as shown above. Start with modest values (e.g., maxThreads=150, minSpareThreads=20) and adjust based on observed load and response times. Keep an eye on cross‑connector interference; if latency spikes become unacceptable, revert to per‑connector pools or implement connector‑specific acceptance limits. This approach gives you a single point of control, reduces memory overhead, and makes future capacity planning more straightforward.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.