Spring Boot gets almost all the way through startup, then gives up:
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.Underneath is a PortInUseException from the embedded web server. The operating system refused to let Tomcat (or Jetty, or Netty) bind a listening socket to port 8080 because another process already owns it. Your code is fine. The question is who holds the port and whether that process should be running.
Quick fix checklist
- Look for an earlier run of the same app: a second IDE run tab, a
./mvnw spring-boot:runin another terminal, or a backgroundjava -jar. - Find the owner:
lsof -nP -iTCP:8080 -sTCP:LISTENon macOS/Linux,Get-NetTCPConnection -LocalPort 8080on Windows. - Stop it gracefully (
kill <pid>,Stop-Process -Id <pid>) and only force-kill if it ignores you. - If the other process is legitimate (Jenkins, another service, a Docker container), change this app’s port with
server.port. - In tests, use
@SpringBootTest(webEnvironment = RANDOM_PORT)orserver.port=0.
Before you start
You need a terminal and permission to inspect processes on your machine. On Linux, seeing processes owned by other users may need sudo. This article applies to any Spring Boot 3.x web application using an embedded server; examples use Tomcat, the default with spring-boot-starter-web.
Why it happens
A TCP port can have only one listening socket per address. When Spring Boot’s web server starts, it binds 0.0.0.0:8080 by default. If any process already listens on that port, the bind fails with java.net.BindException: Address already in use, Spring Boot wraps it in PortInUseException, and a FailureAnalyzer prints the friendly message above.
The common owners, roughly in order of frequency:
- Your own previous run. The IDE’s stop button was never pressed, a run configuration allows parallel instances, or a terminal session still has
spring-boot:runrunning. - A crashed or detached JVM. A process started with
nohupor by a build tool that did not shut it down. - Another local service. Tomcat installed as a system service, Jenkins, a dev proxy, or a Docker container publishing
-p 8080:8080. - Two apps sharing the default. Microservices that all inherited
8080and are started together.
DevTools restarts are rarely the cause. A DevTools restart happens inside the same JVM: it closes the old application context (stopping Tomcat and releasing the port) before creating the new one. If you see this error during DevTools restarts, the usual culprit is a slow or blocked shutdown, for example a non-daemon thread or a bean whose shutdown hangs, so the old server has not released the port yet.
Step-by-step walkthrough
Step 1: Confirm the port and the failing app
The line just before the failure tells you what Boot tried: Tomcat initialized with port 8080 (http). If you expected another port, your server.port setting was not applied (wrong profile, typo, or a SERVER_PORT environment variable overriding it). If 8080 is intended, move on to finding the owner.
Step 2: Find the process on macOS and Linux
# macOS and Linux
lsof -nP -iTCP:8080 -sTCP:LISTEN
# Linux alternative
ss -ltnp 'sport = :8080'
# Which Java processes are running, with main class or jar
jps -llsof prints the command and PID, for example java 48213 aditya ... TCP *:8080 (LISTEN). If the command is java, cross-check the PID with jps -l to see which application it is. If it is com.docker.backend or docker-proxy, a container owns the port; docker ps --filter publish=8080 shows which.
Step 3: Find the process on Windows
In PowerShell:
Get-NetTCPConnection -LocalPort 8080 -State Listen |
Select-Object LocalAddress, LocalPort, OwningProcess
Get-Process -Id <pid>In cmd.exe:
netstat -ano | findstr :8080
tasklist /FI "PID eq <pid>"The last column of netstat -ano is the PID. Check the process name before you stop it. PID 4 (System) means the HTTP.sys kernel driver holds the port on behalf of a Windows service such as IIS; reconfigure or stop that service rather than trying to kill the process.
Step 4: Stop it safely, or move your app
If it is a stray copy of your own app, stop it gracefully so it can finish requests and release resources:
kill 48213 # SIGTERM: Spring Boot runs shutdown hooks
kill -9 48213 # only if it ignores SIGTERMOn Windows, Stop-Process -Id <pid> (add -Force only if needed) or taskkill /PID <pid> then taskkill /PID <pid> /F.
If the other process should keep running, change your app’s port instead:
server.port=8081java -jar target/orders.jar --server.port=8081
SERVER_PORT=8081 ./mvnw spring-boot:runStep 5: Prevent it from recurring
Give each service a fixed, documented port in its application.yml, so two services never share the default. In IntelliJ IDEA, keep “Allow multiple instances” unchecked in run configurations you do not intend to run twice, so a second start replaces the first. For tests, never depend on a fixed port.
Worked scenario
A developer works on two services: orders and inventory. Both are Spring Boot 3 apps with no server.port set. They start inventory from a terminal with ./mvnw spring-boot:run, switch to the IDE, and run orders. It fails with “Port 8080 was already in use”.
Diagnosis:
$ lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 51877 aditya 41u IPv6 0x... 0t0 TCP *:8080 (LISTEN)
$ jps -l
51877 com.shop.inventory.InventoryApplication
52011 org.jetbrains.jps.cmdline.LauncherThe owner is inventory, which should keep running. Killing it would just move the problem. The fix is configuration, in each service:
# inventory: src/main/resources/application.yml
server:
port: 8082# orders: src/main/resources/application.yml
server:
port: 8081The team also records the port map in the repository README, and the frontend’s dev proxy points at the new ports. Both services now start in any order.
Common mistake
The most tempting wrong fix is reflexively running kill -9 $(lsof -t -i:8080). It kills every process with a connection on that port, not just the listener (a browser or another client with an open connection to 8080 can be included), and -9 skips Spring Boot’s graceful shutdown, so in-flight requests fail and resources such as connection pools are not closed cleanly. Identify the listener first, and send SIGTERM before SIGKILL.
Another mistake is “fixing” the error by setting server.port=0 in application.properties for the main app. Random ports are perfect for tests and for service-discovery setups, but a local frontend, a reverse proxy or a teammate’s bookmark can no longer find the app.
Finally, restarting the computer works, but you learn nothing, and the stray process (a forgotten run configuration or a service that auto-starts) will be back.
Verify the behavior
After stopping the stray process or changing the port, the startup log should show:
Tomcat started on port 8081 (http) with context path '/'
Started OrdersApplication in 2.3 seconds (process running for 2.6)Confirm the listener and a response:
lsof -nP -iTCP:8081 -sTCP:LISTEN
curl -i http://localhost:8081/actuator/healthThe curl check assumes Actuator is on the classpath; any known endpoint works. For tests, let Spring pick a free port and inject it:
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrdersApplicationIT {
@LocalServerPort
int port;
@Autowired
TestRestTemplate rest;
@Test
void startsOnAFreePort() {
assertThat(port).isPositive();
assertThat(rest.getForEntity("/actuator/health", String.class).getStatusCode().is2xxSuccessful())
.isTrue();
}
}TestRestTemplate is pre-configured with the random port, so parallel CI jobs on the same agent never collide.
Interview exercise
“Your Spring Boot integration tests pass locally but fail intermittently in CI with ‘Port 8080 was already in use’. What is going on and how do you fix it properly?”
Answer and reasoning
Intermittent means the port is sometimes taken by something else: most likely several jobs or test JVMs run in parallel on the same CI agent, or a test context with a defined port stays cached while another context tries to bind the same port. Tests that use webEnvironment = DEFINED_PORT or set server.port=8080 will fight over it. The fix is to stop depending on a fixed port: use RANDOM_PORT, inject the port with @LocalServerPort, and let TestRestTemplate, WebTestClient or RestClient built from that port make requests. I would also look for tests that start the app manually with SpringApplication.run and never close it. Killing processes in a CI pre-step would only hide the race.