What Apache Tomcat Actually Does With Your Request (Startup, Request, Shutdown)

October 1, 2026 · How It Actually Works: Real Systems as State Machines (part 5)

▶ Watch on YouTube & subscribe to The Stack Underflow

Every Spring MVC request passes through Tomcat before your controller runs. This video draws standalone Apache Tomcat 11 as one hierarchical state machine: how it starts, how one request travels from the socket to the servlet, and how it stops. This page walks the same path in writing.

The one-line version: standalone Tomcat binds its port in init(), starts children before parents, and carries each request Coyote → CoyoteAdapter → Mapper → Valves → filters → servlet.

Last verified against the Apache Tomcat 11.0 documentation (version 11.0.26) and the 11.0.x source: 1 October 2026. Scope: Tomcat 11, standalone, default NIO HTTP connector, one Host and one web app. Clustering, AJP, HTTP/2 and JSP compilation are out of scope.

Six memory pegs

From the Tomcat words primer, where Tomcat is a hotel. These are analogies, memory aids only.

WordMemory pegWhat it actually is
ServerThe whole buildingThe entire Catalina servlet container
ServiceA wing of the buildingOne or more Connectors tied to exactly one Engine
ConnectorThe entranceListens on one TCP port with one protocol
ContextOne tenant businessOne web application, chosen by its context path
Acceptor / PollerThe doorman / the conciergeAccepts new connections / watches open ones with one NIO selector
Lifecycle statesThe opening and closing procedureThe twelve LifecycleState values every component follows

Who owns whom

In standalone Tomcat, Tomcat owns the process and your web apps are deployed into it. In a Spring Boot jar it is the other way round: Spring Boot creates and owns an embedded Tomcat. That one difference changes when the port is bound (below).

Chapter 1 · Bootstrap

catalina.sh start (or running in the foreground, as container images do) launches org.apache.catalina.startup.Bootstrap. Its main() runs before any Tomcat component exists, and has two jobs:

  1. Build class loaders. By default only the Common loader, over Tomcat’s lib folder, is real; the Server and Shared loaders are not defined and fall back to Common.
  2. Load Catalina through that loader, set it to await a shutdown command, and call load(), then start().

Chapter 2 · load(): server.xml, init, the ports

Catalina.load() parses conf/server.xml into Java objects (Server, Service, Connectors, Engine, Hosts), then calls init() down the tree. Each component goes NEW → INITIALIZING → INITIALIZED.

The detail that explains a classic failure: the connector’s bindOnInit attribute defaults to true. Tomcat’s docs: “If true, it is bound when the connector is initiated … If false, the socket will be bound when the connector is started.” So in standalone Tomcat the port is bound during init(), before any web app deploys. If port 8080 is taken, init fails, Catalina turns that into an error, and Bootstrap exits with status 1.

If server.xml cannot be parsed, there is no Server to start, and Bootstrap also exits with status 1.

Chapter 3 · start(): children first

start() runs down the same tree, with one rule worth memorising: a container starts its children, then its own pipeline, and only then enters STARTING itself. The parent waits in STARTING_PREP while its children run to STARTED, so the parent is the last to finish: Context, then Host, then Engine.

Inside the Service the order is explicit in StandardService.startInternal():

engine.start()           // Hosts, Contexts, Wrappers
executors start
mapperListener.start()   // tells the Mapper about every host, context, servlet
connectors start         // only now can requests arrive

Requests can’t arrive before there is something to map them to.

One web application (Context /orders)

When Host localhost deploys a web app (the video uses orders.war at /orders), the Context:

  1. creates its own web-app class loader (WEB-INF/classes, then WEB-INF/lib, before Common; JVM classes and the Jakarta EE APIs always come from the parent),
  2. reads web.xml, web fragments and annotations, and builds the ServletContext,
  3. calls listeners’ contextInitialized(),
  4. calls every filter’s init(),
  5. initializes load-on-startup servlets in ascending order. Other servlets wait for their first request.

If a listener, filter or startup servlet fails, that web app is marked FAILED and isn’t available. The Host keeps deploying the others; one broken app doesn’t stop Tomcat.

When the connectors start their Poller and Acceptor threads, Catalina registers a JVM shutdown hook and the main thread waits for SHUTDOWN on port 8005.

Chapter 4 · One request, from socket to servlet

The connection. The Acceptor accepts connections until maxConnections (default 8192) are open. At that limit, new connections wait in the operating system’s queue, sized by acceptCount (default 100; the docs note the OS may use a different size). When that queue is full too, new connections may be refused or time out. An accepted connection waits in the Poller until data arrives, then is handed to a worker thread, at most maxThreads (default 200).

The request, on the worker thread:

StepWhat happens
Http11Processor (Coyote)Parses the request line and headers into a low-level Coyote Request
CoyoteAdapter.service()The bridge into Catalina: wraps it in a Catalina request, which your code sees as HttpServletRequest
MapperHost from the Host header (falling back to the default host), Context by longest context-path prefix, Wrapper from the servlet mappings. No matching Context: Tomcat answers 404 itself
Engine, Host, Context pipelinesEach container’s Valves run in order; the basic valves StandardEngineValve, StandardHostValve, StandardContextValve each pass it down a level
StandardWrapperValveAllocates the servlet instance (loading and calling init() once, if this is its first request) and builds the filter chain
ApplicationFilterChainRuns each filter in order. A filter can answer by itself (for example a security filter returning 401) and the servlet never runs. With Spring Security, this is where its filter chain runs
servlet.service()In Spring MVC, this servlet is DispatcherServlet, and Spring takes over

The response goes back out through the filters and Coyote, and the worker thread returns to the pool. If the connection can be kept alive it returns to the Poller; Tomcat closes it after maxKeepAliveRequests (default 100) or after keepAliveTimeout of idle time.

Valves vs filters: Valves are a Tomcat mechanism (the access log valve is one); filters are part of the Servlet API and live in your app.

Chapter 5 · Graceful stop

SHUTDOWN on port 8005 (what catalina.sh stop sends) or the JVM shutdown hook (for example on SIGTERM) starts Catalina.stop(). The Service stops in a careful order:

  1. Connectors pause, so no new requests are processed.
  2. The Engine stops its Hosts and Contexts. Before a servlet’s destroy() runs, its Wrapper waits up to unloadDelay (default 2000 ms) for requests still in progress.
  3. Each Context destroys its filters, then calls listeners’ contextDestroyed(): the reverse of startup.
  4. Connectors stop and close their sockets; executor threads end.
  5. The Server goes DESTROYING → DESTROYED and the JVM exits.

Chapter 6 · The Lifecycle

Every component, from the Server down to each Wrapper, follows the same twelve-state machine. Apart from FAILED, it is one line: NEW; INITIALIZING, INITIALIZED; STARTING_PREP, STARTING, STARTED; STOPPING_PREP, STOPPING, STOPPED; DESTROYING, DESTROYED.

Two rules from the Lifecycle Javadoc: “Any state can transition to FAILED”, and FAILED is not a dead end. Calling stop() on a failed component fires the stop events but moves it directly from FAILED to STOPPING, bypassing STOPPING_PREP, and then to STOPPED, from where it can be destroyed or started again.

Pause & Prove

1. Port 8080 is already taken. Does startup fail in init() or start()? And in Spring Boot’s embedded Tomcat?

  • Standalone fails in init(). ✓ bindOnInit defaults to true, so the connector binds during init; init fails and Bootstrap exits with status 1. No web app ever deploys.
  • Standalone fails in start(). Tempting, because “start” sounds like “open the port”, but that is only true when bindOnInit is false.
  • Spring Boot fails in start(). ✓ Spring Boot sets bindOnInit=false and removes the connectors until its web server starts, so the bind, and the failure, happen at WebServer.start() in finishRefresh(), after your beans are created.
  • Both fail when the first request arrives. No: the port is bound during startup in both cases.

2. Inside one Tomcat web app, what starts first? (Community poll)

  • Filters. Second: each filter’s init() runs after the listeners.
  • Load-on-startup servlets. Third, in ascending order of their values.
  • ServletContextListeners. ✓ contextInitialized() runs first.
  • The connector. Connectors start after the whole Engine, and therefore after every web app.

3. Peg drill

Say the peg for each: the Server, a Connector, a Context, the Acceptor and the Poller. Answers: the whole building; the entrance; one tenant business; the doorman and the concierge.

Before / after this video

Sources

Checked on 1 October 2026 (Tomcat docs version 11.0.26), plus the video’s own verified sources:

Change notes

  • 1 Oct 2026: first published. Added from the HTTP Connector page: the operating system may use a different queue size than acceptCount.

Not affiliated with or endorsed by the Apache Software Foundation or VMware Broadcom (Spring). Apache Tomcat is a trademark of the ASF. Found a mistake? Tell us in the video’s comments and we’ll correct this page.

Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.

Subscribe on YouTube →

Prefer email? Get the free newsletter: one failure, traced step by step, about once a week.