skip to content

Connectors & Thread Pools

This is how Tomcat accepts a socket and hands it to a request thread: the <Connector> element, the NIO/NIO2/APR protocol choice, and the maxThreads / acceptCount / maxConnections / connectionTimeout numbers you get asked to explain under load. Interviewers probe it because a thread-pool-exhausted Tomcat behind a proxy looks like a network problem and almost never is, and because the X-Forwarded handling here decides whether your app ever sees the real client IP or scheme.

on this pageshow

questions

6

In a Tomcat server.xml <Connector>, what do maxThreads, maxConnections and acceptCount each limit, and what happens to an incoming connection as each of those limits is reached in turn?

level: middleimportance: must knowfreq 78%

answer

  1. three gates, not one number
  2. threads run, connections wait
  3. NIO polls idle sockets thread-free
  4. acceptCount is the OS backlog
  5. refused connections come last, not first

basics

~20 s

maxThreads caps how many requests Tomcat processes at once, maxConnections caps how many sockets it holds open, and acceptCount is the OS accept-queue length used once maxConnections is full. Past all three, new connections are refused.

solid answer

~50 s

They are three different queues in front of your servlet. `maxConnections` (default 8192 on the NIO connector) is how many sockets Tomcat will keep open and poll at once; `maxThreads` (default 200) is how many of those connections can be actively executing a request, because each in-flight request occupies one `http-nio-<port>-exec-` thread. Since NIO polls idle connections without a thread, connections and threads are decoupled: you can hold thousands of keep-alive sockets while only 200 run code. When all threads are busy, extra connections just sit accepted and unread — the client sees latency, not an error. When `maxConnections` is reached, Tomcat stops accepting, so new connections queue in the OS accept backlog, whose length is `acceptCount` (default 100). Once that backlog fills, the OS refuses or drops the connection and the client gets a connection error.

code

xml · 12 lines
xml
<Executor name="tomcatThreadPool"
          namePrefix="catalina-exec-"
          maxThreads="200"
          minSpareThreads="25"/>

<Connector executor="tomcatThreadPool"
           port="8080"
           protocol="HTTP/1.1"
           maxConnections="8192"
           acceptCount="100"
           connectionTimeout="20000"
           redirectPort="8443"/>

go deeper

for a junior

Know the three attribute names and that they live on the <Connector> element in server.xml, and be able to say which one caps simultaneous request processing.

for a middle

Explain the order in which the limits bite and the symptom each produces, plus the defaults (200 threads, 8192 connections, 100 accept queue) and why NIO lets connections outnumber threads.

for a senior

Show that you have diagnosed this live: tie rising latency with no errors to thread saturation, connection refusals to a full backlog, and be able to say why the proxy in front reports a gateway error while Tomcat's log looks clean.

for a principal

Own the sizing policy across a fleet: how blocking profile drives pool size, when a shared <Executor> is a false economy because it removes isolation, and why a deep accept backlog trades fast failure for wasted work.

## Three limits, three different queues A request reaching Tomcat crosses three gates, and each has its own attribute on the `<Connector>` element in `server.xml`. Candidates who know only `maxThreads` misdiagnose load problems, because the symptom differs sharply depending on which gate is the one saturated. ```xml <Connector port="8080" protocol="HTTP/1.1" maxThreads="200" minSpareThreads="10" maxConnections="8192" acceptCount="100" connectionTimeout="20000" /> ``` **maxThreads** is the maximum number of request-processing threads this connector will create — therefore the maximum number of requests being *executed* at one instant. The default is 200. Each thread is named `http-nio-8080-exec-N`, which is how you recognise them in a thread dump. Note that if the connector references a shared `<Executor>`, the connector's own `maxThreads` is ignored and the executor's value governs. **maxConnections** is the maximum number of connections the connector will accept and keep open at one time. On the NIO connector the default is 8192. This number is much larger than `maxThreads` on purpose (see below). **acceptCount** is the length of the operating system's accept queue — the backlog Tomcat asks for when it binds the socket — and it only matters once `maxConnections` has been reached and Tomcat has stopped accepting. The default is 100. The OS can impose its own ceiling on the requested backlog. **minSpareThreads** (default 10) is unrelated to capacity: it is the number of threads kept alive even when idle, so a burst does not pay thread-creation cost on every request. ## Why connections and threads are decoupled Older Tomcat used one thread per connection, which meant an idle keep-alive connection wasted a thread. The NIO connector instead has a small number of poller threads watching sockets for readable data; a request thread is taken from the pool only when a complete request is ready to run, and handed back when the response is written. That is why `maxConnections` can sensibly be forty times `maxThreads`: ten thousand mostly-idle browser connections cost sockets and buffers, not threads. The practical consequence is that **thread starvation is invisible from the outside as an error**. If all 200 exec threads are blocked on a slow database call, connection 201 is still accepted and read — it simply waits. Clients experience growing latency; a proxy in front eventually gives up on its own timeout and reports a gateway error, while Tomcat's access log shows nothing at all until the request finally completes, because Tomcat writes the log entry after the response. ## The journey of one connection as load climbs 1. Under normal load: connection accepted, request read, an exec thread runs the servlet, response written, connection kept alive for the next request. 2. All exec threads busy: the connection is still accepted, but the parsed request waits internally for a free thread. Symptom: latency rises, error rate stays zero. 3. `maxConnections` reached: Tomcat stops calling accept. New connections pile up in the kernel accept queue. Symptom: connect succeeds (the OS completes the handshake) but nothing is read for a long time. 4. Accept queue full (`acceptCount` exceeded): the OS refuses or silently drops the connection. Symptom: connection refused, or connect timeouts — this is the first point at which the failure looks like a *network* problem, which is exactly why the whole chain gets misdiagnosed. ## Sizing, and the shared Executor `maxThreads` should follow the request's blocking profile, not a folklore number. If a request spends 90% of its time waiting on a downstream call, threads are cheap concurrency and a larger pool helps up to the point the downstream falls over; if requests are CPU-bound, a pool far above the core count only adds context switching and queueing. Raising `maxThreads` never fixes a saturated dependency — it just points more concurrency at it. Several connectors can share one pool with `<Executor>`: ```xml <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="200" minSpareThreads="25"/> <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"/> ``` Sharing saves threads but removes isolation: a slow endpoint on one connector can consume the pool that another connector depends on. Tomcat's executor also differs from a stock JDK thread pool in that it grows the pool up to `maxThreads` *before* queueing tasks, which is the behaviour you want for a request server. Finally, `acceptCount` is a shock absorber, not capacity. A large backlog converts refusals into long waits, which for a client that has already given up is worse than a fast failure — the request is executed by Tomcat after nobody is listening for the answer.

  • If a connector declares both an executor and its own maxThreads, which one wins?
    The `<Executor>`'s. When a connector references a shared executor, the connector's `maxThreads` and `minSpareThreads` are ignored and the executor's values govern the pool. It is a common misconfiguration to tune the connector attribute and see no effect at all, because the pool being used belongs to the executor.
  • Why does raising acceptCount rarely help a service that is already saturated?
    Because the backlog stores work nobody is still waiting for. Connections sit in the queue past the client's own timeout, then get accepted and executed anyway, so the server burns capacity producing responses that are thrown away. A shorter backlog sheds load fast and lets the caller retry or fail cleanly.
  • You see latency climbing but zero errors from Tomcat. Which limit is the suspect?
    maxThreads. Connections are still being accepted and read, so nothing is refused; requests are simply queueing for a free exec thread. Confirm with a thread dump showing all `http-nio-<port>-exec-` threads busy, or the `currentThreadsBusy` attribute on the connector's ThreadPool MBean sitting at `maxThreads`.

saying these in an interview costs you the question

  • Thinks maxThreads and maxConnections mean the same thing
  • Says Tomcat returns 503 once maxThreads is reached
  • Believes each open keep-alive connection holds a request thread
  • Treats acceptCount as extra request capacity
  • Answers every load problem by raising maxThreads

context

open as a page

A Java service on Tomcat behind a reverse proxy starts returning gateway timeouts to users, yet Tomcat's own logs look quiet, CPU is near idle and the host has plenty of memory. How do you confirm the connector's thread pool is exhausted, and what do you do once you have?

level: seniorimportance: must knowfreq 57%

basics

~20 s

Take a thread dump and count the connector's exec threads: if all maxThreads are alive and blocked in the same downstream call, the pool is exhausted. Confirm with the ThreadPool MBean's currentThreadsBusy, then bound the downstream call rather than enlarging the pool.

open as a page

In a Tomcat server.xml <Connector>, what does the connectionTimeout attribute actually time out, and how does keepAliveTimeout differ from it?

level: juniorimportance: should knowfreq 45%

basics

~20 s

connectionTimeout is how long Tomcat waits, after accepting a connection, for the request line to arrive. keepAliveTimeout is how long it waits for the next request on an already-used connection, and defaults to the connectionTimeout value.

open as a page

A Spring app on Tomcat sits behind a TLS-terminating reverse proxy. It logs the proxy's IP address as every user's address and builds http:// links even though users arrived over HTTPS. Which Tomcat-side configuration fixes the client IP and scheme, and how do you keep an attacker from spoofing it?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Enable Tomcat's RemoteIpValve so forwarded headers rewrite the request's remote address, scheme, port and secure flag; restrict trust with its internalProxies and trustedProxies patterns. A static alternative is the connector's scheme, secure, proxyName and proxyPort attributes.

open as a page

On a Tomcat service, one endpoint spends nearly all of each request blocked on a slow third-party API and its exec threads are saturated. How do you decide between raising maxThreads, converting the endpoint to an async servlet, and giving it its own <Executor> or connector?

level: principalimportance: should knowfreq 36%

basics

~20 s

Decide by what the threads are waiting on and who else shares the pool. Async frees the exec thread during the wait and lifts a throughput ceiling; a separate Executor buys isolation; a bigger pool only helps when the dependency can absorb more concurrency and the threads themselves are cheap.

open as a page

A Tomcat <Connector> element's protocol attribute can be set to "HTTP/1.1", to org.apache.coyote.http11.Http11Nio2Protocol, or to the APR/native implementation. What do those choose between, and which would you deploy today?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

They select Tomcat's socket-handling implementation. NIO uses Java non-blocking sockets with poller threads and is the default; NIO2 uses the JDK's asynchronous channel API; APR/native used the C Apache Portable Runtime and is gone in current Tomcat. Deploy NIO.

open as a page