skip to content

Embedded Tomcat in Spring Boot

Most engineers meet Tomcat as the embedded server inside a Spring Boot fat jar, never touching server.xml — so the questions become which server.tomcat.* properties map to which classic connector setting, how graceful shutdown works, and when you would swap in Jetty or Undertow. Interviewers use it to check you understand that the container is still there, still has a bounded thread pool, and still needs tuning even though nobody edited an XML file.

on this pageshow

questions

6

A Spring Boot application built with spring-boot-starter-web starts with `java -jar app.jar` on a host where no Tomcat is installed, and logs "Tomcat started on port 8080". What is actually serving HTTP, and what does setting `server.port=0` do?

level: juniorimportance: must knowfreq 72%

answer

  1. the server ships inside the jar
  2. no server.xml, no webapps directory
  3. one property picks the listener port
  4. zero is a socket idiom, not a switch

basics

~20 s

spring-boot-starter-web pulls Tomcat in as an ordinary library, so the application starts a real Tomcat inside its own JVM process on port 8080. Setting server.port=0 makes it bind a random free port chosen by the operating system.

solid answer

~40 s

There is a real Tomcat there — it is just embedded rather than installed. `spring-boot-starter-web` depends on `spring-boot-starter-tomcat`, which brings the `tomcat-embed-core` jars onto the classpath; at startup Spring Boot sees a servlet stack, builds a web server factory, and starts Tomcat in-process. The host needs only a JRE: no distribution, no `server.xml`, no `webapps` directory, and the application owns the server's lifecycle instead of the other way round. `server.port` chooses the listener port and defaults to 8080; it can be set in `application.properties`, as the `SERVER_PORT` environment variable, or as `--server.port=9000` on the command line, so the same artifact runs anywhere. `server.port=0` asks the OS for an ephemeral free port — the normal choice for tests, where `@SpringBootTest(webEnvironment = RANDOM_PORT)` plus `@LocalServerPort` reads back the port that was actually assigned.

code

properties · 3 lines
properties
server.port=0
server.address=0.0.0.0
server.servlet.context-path=/api

go deeper

for a junior

Be able to say plainly that the server is a library inside the jar, that the default port is 8080, and that server.port can be overridden by environment variable or command line without rebuilding.

for a middle

Explain the startup sequence — classpath detection, web server factory, connector bound in-process — and why server.port=0 plus @LocalServerPort is the right pattern for integration tests.

for a senior

Show that you still treat the embedded container as a real server: it has a bounded thread pool and an accept queue that need sizing, and its configuration surface simply moved from XML to properties.

for a principal

Own the packaging decision for a fleet: self-contained jars give per-service isolation and independent upgrades, while a shared standalone container centralises patching at the cost of coupling every deployment to one server's lifecycle.

## Embedded rather than deployed The classic model was: install a Tomcat distribution on a server, drop a WAR into `webapps/`, and let Tomcat's classloader find and start your application. Spring Boot inverts that relationship. Tomcat's jars — `tomcat-embed-core`, `tomcat-embed-el`, `tomcat-embed-websocket` — are pulled in as ordinary dependencies by `spring-boot-starter-tomcat`, which `spring-boot-starter-web` includes transitively. Your `main()` method runs first, Spring Boot detects a servlet web application on the classpath, creates a servlet web server factory, and that factory constructs a Tomcat instance, attaches a connector, and starts it inside the same JVM. The practical consequences are large. The deployable unit is one self-contained jar. The host needs only a JRE. There is no shared server whose version, `server.xml`, or global libraries are owned by someone else, and no possibility of one badly behaved application taking down three others deployed beside it. ## What is still true Embedded Tomcat is not a cut-down imitation — it is the same Tomcat code, with the same connector, the same bounded pool of request-processing threads, and the same behaviour under load. That is exactly why interviewers ask this: engineers who have only ever used Boot sometimes talk as if the servlet container disappeared. It did not. It still has a maximum thread count (`server.tomcat.threads.max`, default 200), it still has an accept queue, and it still needs to be sized for the workload. What changed is the configuration surface: settings that used to be XML attributes are now `server.*` and `server.tomcat.*` properties, with a programmatic escape hatch for anything not exposed. ## server.port `server.port` selects the port the embedded connector binds. The default is 8080. Because it is an ordinary Spring `Environment` property, it can come from `application.properties` or `application.yaml`, a profile-specific file, the `SERVER_PORT` environment variable, a JVM system property, or a `--server.port=9000` command-line argument — with command line and environment overriding the packaged file. The same immutable artifact therefore runs on a different port in a different environment with no rebuild, which is the whole point of externalized configuration. ```properties server.port=9000 server.address=127.0.0.1 server.servlet.context-path=/api ``` `server.address` restricts which interface is bound — useful when the process should only be reachable from a proxy on the same host. `server.servlet.context-path` prefixes every mapping, which is how you reproduce the old "application deployed under /api" arrangement without a WAR. ## server.port=0 Setting the port to `0` is a standard socket idiom: the OS picks any free ephemeral port at bind time. Spring Boot logs the port it actually got, and in tests the value is injected: ```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ApiIT { @LocalServerPort int port; } ``` This is what makes parallel or CI test runs safe — two builds on the same machine cannot collide on a hardcoded 8080. It is also occasionally used by services that register their own address with a discovery registry after startup, since the registered port is discovered rather than fixed. It is the wrong choice anywhere the port must be predictable: a firewall rule, a proxy upstream, or a container port mapping all need a known number. ## Jar or war Boot can still produce a WAR for a standalone Tomcat: set the packaging to `war`, mark `spring-boot-starter-tomcat` as `provided` so the embedded copy is not bundled twice, and have the main class extend `SpringBootServletInitializer` so an external container can bootstrap the application context. The result is still executable as a jar. This exists mostly for organisations with a mandated shared application server; the default and overwhelmingly common path is the executable jar with the container inside it. ## The startup log line "Tomcat started on port 8080 (http)" is printed after the connector has successfully bound. A failure here — `Web server failed to start. Port 8080 was already in use.` — is a bind failure of the embedded connector in your own process, not a problem with an installed server, and the fix is to change `server.port` or stop whatever already holds the socket.

  • How would you take that same Spring Boot codebase and deploy it into a standalone Tomcat instead?
    Change packaging to `war`, mark `spring-boot-starter-tomcat` as `provided` so the embedded jars are not shipped inside the war, and extend `SpringBootServletInitializer` in the main class so the external container can bootstrap the application context. The artifact stays executable as a jar too. It is a legitimate path for organisations mandating a shared application server, but you give up self-contained deployment.
  • Where do you set the port when the application runs in a container image you cannot rebuild?
    As the `SERVER_PORT` environment variable, or a `--server.port=` argument appended to the command. Spring Boot's property resolution puts both above the packaged `application.properties`, which is why the same image runs on any port. Nothing in the jar has to change.
  • The application fails at startup with "Port 8080 was already in use". What is happening?
    The embedded connector could not bind its socket, because another process — often a previous run of the same application that never exited — already holds the port. It is a failure inside your own JVM, not a problem with any installed server. Kill the holder, or set a different `server.port`.

saying these in an interview costs you the question

  • Thinks Tomcat must be installed on the host machine
  • Believes the fat jar is deployed into a webapps directory
  • Says server.port=0 disables the web server
  • Claims embedded Tomcat is a different, lighter product than standalone Tomcat
  • Assumes changing the port requires rebuilding the artifact

context

open as a page

A Spring Boot service on embedded Tomcat stops answering new requests under load while its CPU stays near idle. Which `server.tomcat.*` properties bound how many requests it can handle at once, and what happens to a request that arrives past each bound?

level: middleimportance: must knowfreq 66%

basics

~20 s

Three properties bound it: server.tomcat.max-connections (default 8192) caps accepted connections, server.tomcat.threads.max (default 200) caps requests processed at once, and server.tomcat.accept-count (default 100) sizes the OS backlog beyond that. Idle CPU means the 200 threads are blocked, not busy.

open as a page

You need to configure something on Spring Boot's embedded Tomcat that no `server.*` property exposes — for example an additional plain-HTTP connector alongside the main one. How do you do it in code, and how does that interact with the `server.*` properties already set?

level: middleimportance: should knowfreq 38%

basics

~10 s

Register a WebServerFactoryCustomizer<TomcatServletWebServerFactory> bean. It receives the factory before the server is built, so you can call addAdditionalTomcatConnectors or addConnectorCustomizers. It runs after the property-driven customizer, so code overrides server.* values.

open as a page

During rolling restarts, a Spring Boot service on embedded Tomcat returns errors for requests that were already in flight. What does `server.shutdown=graceful` change about how the embedded container stops, which property bounds it, and why is that setting alone often not enough?

level: seniorimportance: should knowfreq 50%

basics

~20 s

With server.shutdown=graceful the embedded container stops accepting new requests and lets in-flight ones finish, bounded by spring.lifecycle.timeout-per-shutdown-phase (default 30 seconds). It does not stop the load balancer routing new traffic, so a pre-shutdown delay is still needed.

open as a page

Setting `spring.threads.virtual.enabled=true` on a Spring Boot 3.2+ application running Java 21 changes how the embedded Tomcat handles requests. What changes, and which setting then bounds how many requests run at once?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Spring Boot gives embedded Tomcat a virtual-thread-per-task executor, so each request gets a fresh virtual thread instead of one from the fixed pool. server.tomcat.threads.max stops bounding concurrency; server.tomcat.max-connections and accept-count become the real admission limits.

open as a page

A team proposes replacing embedded Tomcat with Undertow or Jetty across a fleet of Spring Boot services, arguing it will improve throughput. How would you evaluate that proposal, and what does the swap actually cost?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

The swap is mechanically trivial — exclude the Tomcat starter, add the Jetty or Undertow one — but the container is rarely the bottleneck. The real cost is that every server.tomcat.* property and Tomcat-typed customizer silently stops applying, taking tuning with it.

open as a page