Technical deep dive
Tomcat DDoS Hardening: Connector Thread Pools, Timeouts and the Front Layer
Last updated: August 2026 · Connector pool, timeouts and the mandatory front layer · Reading time ~18 min

Tomcat's DDoS exposure lives in its Connector. maxThreads, acceptCount, maxConnections and connectionTimeout are the load-bearing settings, and the NIO protocol keeps a slow connection from costing a whole thread. But the largest single decision is architectural: a bare Tomcat on port 8080 facing the internet is exposed in ways no Connector value fixes, so a front layer comes first.
This guide hardens Apache Tomcat itself against the connection- and thread-exhausting part of a DDoS attack. It differs from the web-server guides in one structural way that has to be said first: Tomcat is an application server, and the standard production design does not expose it directly. A hardened web server or load balancer sits in front, terminates the connection, and forwards clean requests over a private network. Most of this series’ defences live in that front layer.
So the Connector tuning below is the second line, applied behind the front layer. It matters — a front layer can fail, be bypassed on an internal network, or forward an attack it did not catch — but it is not a substitute for putting a hardened layer in front. Every setting comes with its value and the JMX or log counter that shows it working.
| Appliance | Attack shape | Tomcat setting | Verification |
|---|---|---|---|
| Slow-header / slow-body (Slowloris) | connectionTimeout; NIO protocol; keepAliveTimeout | Manager: threads in R state; access log %D | |
| Connection flood | maxConnections; acceptCount (OS backlog) | jmx Connector connectionCount | |
| Thread-pool exhaustion | maxThreads; a shared Executor across connectors | jmx ThreadPool currentThreadsBusy | |
| Large-request abuse | maxHttpHeaderSize, maxParameterCount, maxPostSize | 400 rate in access log | |
| Direct internet exposure | Do not expose 8080; a front layer terminates first | netstat: 8080 bound to localhost/internal only |
The last row is the decision that reframes the others: Tomcat is an application server, and the standard, most defensible design puts a hardened web server or proxy in front of it. The Connector tuning below assumes that front layer exists.
0. Baseline first: expose the Connector metrics
Tomcat’s live state is in JMX and the access log. Turn on an access log that records processing time, and read the Connector’s thread pool over a normal week:
<!-- server.xml, inside <Host> — %D is request time in ms, %S the session -->
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs" prefix="access." suffix=".log"
pattern="%h %t "%r" %s %b %D" />
# Thread pool and connection count via JMX (jconsole, or jmxterm headless)
# Catalina:type=ThreadPool,name="http-nio-8080" -> currentThreadsBusy, connectionCount
# Slowest requests from the access log (last field is %D, ms)
awk '{print $NF, $0}' logs/access.*.log | sort -rn | head
# Per-client request counts, a normal week
awk '{print $1}' logs/access.*.log | sort | uniq -c | sort -rn | head
currentThreadsBusy against maxThreads, and connectionCount against maxConnections, are the
two baselines every value below is set against.
1. The front layer comes first
Before touching server.xml, confirm Tomcat is not directly exposed:
# The HTTP Connector should bind to localhost or an internal address, never 0.0.0.0 on a public host
ss -ltnp | grep 8080
If 8080 is bound to a public interface, that is the first thing to fix — bind it to the internal address the front layer uses, and let nginx or Apache httpd terminate the internet-facing connection. Those front layers are hardened in the nginx and Apache guides, and they carry TLS, slow-connection eviction and rate limiting far better than the Connector does.
<!-- server.xml — bind to the internal interface only -->
<Connector port="8080" address="10.0.0.5"
protocol="org.apache.coyote.http11.Http11NioProtocol"
... />
2. The Connector protocol: NIO, not blocking
The protocol attribute decides whether a slow connection costs a thread. Use the NIO connector; the old blocking connector spent a thread per connection and made Slowloris cheap:
<Connector port="8080" address="10.0.0.5"
protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
maxThreads="400"
minSpareThreads="25"
maxConnections="8192"
acceptCount="200"
maxKeepAliveRequests="100"
keepAliveTimeout="15000" />
# Confirm the running protocol
grep -i 'protocol=' conf/server.xml
# In the log at startup: "Initializing ProtocolHandler [http-nio-8080]"
grep -i 'ProtocolHandler' logs/catalina.out | tail
3. The three connection limits, sized together
maxConnections, acceptCount and maxThreads are three limits in sequence — connections held,
connections backlogged, requests processed. Size them as a set:
maxConnections = 8192 # connections accepted and held at once
acceptCount = 200 # OS backlog once maxConnections is reached (maps to the listen backlog)
maxThreads = 400 # requests processed concurrently
minSpareThreads = 25 # threads kept warm
A large maxConnections behind a small maxThreads is not protection; it is a long queue of
connections waiting on a few threads, which fails slowly and confusingly. Size maxThreads to what
the application and heap actually support under load, then set maxConnections a healthy multiple
above it and acceptCount as a short backlog, not a large one — a large backlog just delays the
refusal that protects the pool.
# Busy threads vs ceiling, live
# JMX: Catalina:type=ThreadPool,name="http-nio-8080" currentThreadsBusy / maxThreads
# When busy == maxThreads and connectionCount climbs, the pool is the bottleneck
4. A shared Executor across connectors
If the server has more than one connector (HTTP and HTTPS), a shared Executor bounds total
threads across both instead of each connector holding its own pool:
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="400" minSpareThreads="25"
maxIdleTime="60000" prestartminSpareThreads="true" />
<Connector executor="tomcatThreadPool" port="8080" ... />
<Connector executor="tomcatThreadPool" port="8443" ... />
This prevents a flood on one connector from starving the other, and gives a single number to size against the heap.
5. Timeouts and request-size limits
connectionTimeout is the Slowloris backstop; the size limits close the oversized-request surface:
<Connector ...
connectionTimeout="20000" <!-- ms to receive the request line + headers -->
keepAliveTimeout="15000" <!-- ms idle before closing a keep-alive connection -->
maxKeepAliveRequests="100"
maxHttpHeaderSize="8192" <!-- bytes -->
maxParameterCount="1000" <!-- request parameters; caps hash-collision abuse -->
maxPostSize="2097152" <!-- 2 MB request body -->
maxSwallowSize="2097152" />
maxParameterCount is worth setting explicitly: an unbounded parameter count is a cheap way to
burn CPU in request parsing. A negative value means unlimited, which is the wrong default for an
internet-adjacent server even behind a proxy.
# Oversized/rejected requests appear as 400 in the access log
awk '$4==400' logs/access.*.log | wc -l
6. Client IP behind the front layer
With a front layer present, every access log entry and every RemoteAddrValve rule sees the proxy unless RemoteIpValve is configured:
<!-- server.xml, inside <Host> -->
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto"
internalProxies="10\.\d+\.\d+\.\d+|172\.1[6-9]\.\d+\.\d+" />
Without it, the logs attribute every request to the front layer and any address-based rule is meaningless — the recurring proxy-IP failure across this whole series.
7. Optional: StuckThreadDetectionValve
Under an attack that pushes the application into slow or hung requests, the stuck-thread valve logs threads that exceed a threshold, turning an invisible thread leak into an alert:
<Valve className="org.apache.catalina.valves.StuckThreadDetectionValve"
threshold="60" />
The signals to wire into monitoring
# 1. Busy threads vs maxThreads (JMX currentThreadsBusy) — pool saturation
# 2. connectionCount vs maxConnections (JMX) — connection-hold attack
# 3. 400 rate — oversized/malformed request flood
awk '$4==400' logs/access.*.log | wc -l
# 4. Slow requests — %D column climbing
awk '{print $NF}' logs/access.*.log | sort -rn | head -1
currentThreadsBusy at maxThreads with a climbing connectionCount is the signature that the
request rate has outgrown the pool — the point at which the front layer or an upstream tier has to
absorb what Tomcat cannot.
The honest limit of Tomcat hardening
Everything here defends the application tier against connection and thread-pool exhaustion, behind a front layer that should be catching most of it first. Two cases sit outside it.
The first is circuit saturation: volume that fills the pipe reaches neither the front layer nor Tomcat.
The second is everything beneath Tomcat and its front layer — the OS TCP stack and, on the front host, its own hardening, covered in the Linux or Windows guides. Tomcat hardening assumes both the front layer and the OS layer are in place. Above the circuit, the answer is the network tier.
Order of application
- Confirm the Connector is not bound to a public interface; put a hardened front layer in front.
- Record the baseline: busy threads, connection count, request times.
- Set the Connector protocol to NIO if it is not already.
- Size maxThreads to the heap, then maxConnections and a short acceptCount around it.
- Configure RemoteIpValve so logs and rules see the real client.
- Set connectionTimeout and the request-size limits.
- Wire the four signals into monitoring.
Step 1 is the one that most changes the outcome: the Connector settings that follow are the second line, and a second line is only as useful as the first line it sits behind.
Frequently asked questions
- Should Tomcat ever face the internet directly?
- Rarely, and it is worth being direct about it. Tomcat is an application server; the standard production design terminates connections at a hardened front layer — nginx, Apache httpd, or a load balancer — which handles TLS, slow-connection eviction, rate limiting and static content, and forwards clean requests to Tomcat over a private network. That front layer is where most of the DDoS defences in this series actually live. Exposing Tomcat's 8080 Connector straight to the internet means re-implementing all of that in the Connector, which it does less well. The Connector hardening here is the second line, applied behind the front layer, not a replacement for it.
- NIO, NIO2 or APR — which Connector protocol for resilience?
- NIO or NIO2, not the old blocking connector. The blocking BIO connector spent a thread per connection for the whole request, which made Slowloris cheap to run against it; it was removed in Tomcat 8.5. NIO and NIO2 use non-blocking I/O and a poller, so a slow connection holds a socket, not a dedicated worker thread, the same architectural advantage nginx has. NIO is the default and the right choice for almost everyone; the differences between NIO and NIO2 are minor for this purpose. Confirm you are on one of them and not a legacy configuration pinned to BIO.
- What is the relationship between maxThreads, maxConnections and acceptCount?
- Three limits in sequence. maxConnections is how many connections Tomcat will accept and hold at once; acceptCount is the OS-level backlog of connections waiting to be accepted once maxConnections is reached; maxThreads is how many requests can be actively processed. Under a flood, connections fill up to maxConnections, then queue in acceptCount, then are refused by the OS. maxThreads is the processing ceiling behind that. Sizing them together matters: a huge maxConnections with a small maxThreads just means many connections waiting on a few threads, which is its own slow failure.
- How do I stop a slow-request attack on Tomcat specifically?
- With connectionTimeout, and with the NIO connector underneath it. connectionTimeout caps how long Tomcat waits for the request line and headers after a connection opens; a Slowloris client that dribbles headers is dropped at the timeout. Keep it in the low tens of seconds. On NIO the stalled connection was cheap to begin with, so the timeout is highly effective; the combination is Tomcat's equivalent of nginx's client_header_timeout on an event loop. If a front layer is present, it should be catching this first, and the Connector timeout is the backstop.
- Where does the client IP come from behind a proxy?
- From the RemoteIpValve, which must be configured or every per-IP decision and every access log entry records the proxy address. RemoteIpValve reads X-Forwarded-For and X-Forwarded-Proto and sets the request's remote address to the real client, so access logs, any RemoteAddrValve rules and the application all see the actual source. Without it, a front layer hides every client behind one address — the same failure mode as a misconfigured real_ip in nginx.
- Do these settings stop a volumetric attack?
- No. If the attack fills the circuit in front of the servers, neither the front layer nor Tomcat receives the requests and no Connector value applies. Everything here defends against connection and thread-pool exhaustion at the application tier. Volume above the circuit is a network-tier problem and is not solved in server.xml.
Published: August 2026
This guide is updated as vendors release new models and pricing. How we compare vendors