Ch. 8 · Spring Boot

Web Server Failed to Start: Port 8080 Was Already in Use

Fix 'Web server failed to start. Port 8080 was already in use' in Spring Boot: find the process on the port, stop it or change server.port.

~6 min readbeginnerupdated Oct 4, 2026

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.
Text

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:run in another terminal, or a background java -jar.
  • Find the owner: lsof -nP -iTCP:8080 -sTCP:LISTEN on macOS/Linux, Get-NetTCPConnection -LocalPort 8080 on 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) or server.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:

  1. 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:run running.
  2. A crashed or detached JVM. A process started with nohup or by a build tool that did not shut it down.
  3. Another local service. Tomcat installed as a system service, Jenkins, a dev proxy, or a Docker container publishing -p 8080:8080.
  4. Two apps sharing the default. Microservices that all inherited 8080 and 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 -l
Terminal

lsof 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>
Text

In cmd.exe:

netstat -ano | findstr :8080
tasklist /FI "PID eq <pid>"
Text

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 SIGTERM
Terminal

On 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=8081
properties
java -jar target/orders.jar --server.port=8081
SERVER_PORT=8081 ./mvnw spring-boot:run
Terminal

Step 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.Launcher
Terminal

The 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
yaml
# orders: src/main/resources/application.yml
server:
  port: 8081
yaml

The 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)
Text

Confirm the listener and a response:

lsof -nP -iTCP:8081 -sTCP:LISTEN
curl -i http://localhost:8081/actuator/health
Terminal

The 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();
    }
}
java

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.

Continue learning

More in Spring Boot

read ✓Spring Boot · hard

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readread →
read ✓Spring Boot · hard

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readread →
read ✓Spring Boot · hard

Spring Declarative HTTP Clients

Define outbound HTTP as an annotated interface with @HttpExchange, create the proxy, and configure timeouts and errors.

~2 min readread →
esc