Home › Java › Port 8080 Already in Use — Spring Boot Fix
Beginner 5 min · September 23, 2026

Port 8080 Already in Use — Spring Boot Fix

Run lsof -ti:8080 | xargs kill -9 to free the port, or set server.port=8081 to move your app.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 12 min
  • ✓Java 17 and a Spring Boot project that runs locally
  • ✓Terminal access with lsof or ss installed
  • ✓Basic comfort editing application.properties
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Your app failed because something already holds TCP port 8080, so embedded Tomcat can't bind — it's a conflict, not broken code
  • Find the holder with lsof -i :8080 or ss -ltnp 'sport = :8080', then stop it with kill , escalating to kill -9 only for stuck JVMs
  • Move your app instead with server.port=8081 in application.properties or SERVER_PORT=8081 as an env var
  • In tests use @SpringBootTest(webEnvironment = RANDOM_PORT) so each run gets a free port and parallel builds never clash
✦ Definition~90s read
What is Spring Port 8080 Already in Use Fix?

Every TCP connection on your machine is addressed by an IP plus a 16-bit port number, and port 8080 is Spring Boot's conventional HTTP default. When the embedded Tomcat (or Jetty, or Undertow) starts, it calls the bind syscall to claim exclusive ownership of that port for listening.

★
Think of port 8080 as a single parking space in front of your shop.

The operating system grants it to exactly one socket: a second bind for the same port is rejected with EADDRINUSE, which Java surfaces as java.net.BindException and Spring wraps as Web server failed to start. Port 8080 was already in use.

That exclusivity is a feature, not a bug — it guarantees every incoming request reaches exactly one owner instead of being split mysteriously. The holder can be anything: your own earlier ./mvnw spring-boot:run that never exited, a second microservice defaulting to the same port, a Docker container publishing host 8080, a test JVM, or an unrelated desktop tool.

Spring Boot resolves which port to bind through a fixed precedence chain (CLI args, SERVER_PORT env var, profile properties, application.properties), so the conflict sometimes follows you to a new port when an override is stale.

The fix space has exactly two moves, and this article covers both: evict the holder (find with lsof or ss, stop with kill, verify the port is clear) or move your app (server.port, SERVER_PORT, or RANDOM_PORT for tests). Knowing which move fits — kill what's yours, move around what isn't — is the whole skill, and it transfers to every port conflict you'll ever meet in any stack.

Plain-English First

Think of port 8080 as a single parking space in front of your shop. Your Spring Boot app is a delivery van that needs that exact space. If another van is already parked there — maybe your own van from this morning that you forgot to move — the new van can't park, and the city (your operating system) turns it away. The fix is simple: find whose van is in the space and move it, or tell your van to park in space 8081 instead.

You hit run, the Spring Boot banner prints, and then the startup dies: Port 8080 was already in use. No controller loads, no actuator endpoint responds, and the log buries the real cause two stack frames deep. Every Spring developer meets this error, usually on a deadline, usually when a demo is due in ten minutes.

The error looks scary but it's good news: your code compiled, the context started loading, and the embedded Tomcat got all the way to binding its socket. The OS simply said no, because one socket already listens on 8080. Something else owns the port — a previous run you forgot, a teammate's service, a Docker container, or a test JVM still shutting down.

This guide walks you through the full fix cycle. You'll learn to read the BindException and tell it apart from lookalikes, find the exact process with lsof or ss, kill it safely (or move your app instead), configure server.port and SERVER_PORT without profile surprises, make tests collision-proof with RANDOM_PORT, and untangle Docker publish conflicts. By the end, a bind failure will cost you thirty seconds instead of thirty minutes.

Reading the BindException: What Port 8080 Already in Use Actually Means

The full error reads Web server failed to start. Port 8080 was already in use, usually followed by Caused by: java.net.BindException: Address already in use. Read it inside-out: the outer line is Spring's friendly wrapper, the Caused by line is the OS telling you the bind syscall was rejected. Everything above the wrapper — bean creation traces, context messages — is noise. Your app got all the way through classloading and context refresh; only the final socket bind failed.

Don't confuse this with its cousins. Connection refused means nothing listens on the port you called — the opposite problem. Address already in use with no Spring wrapper usually comes from plain Java socket code or a test that opens raw sockets. And if the message names a different port, some config already moved your server and the clash followed it — check server.port resolution before hunting processes.

The key insight: this error is always about the environment, never about your business logic. No controller, service, or repository code can cause it. So resist the urge to rebuild, revert, or rebase. The fastest path is a single command that names the holder, and the next section shows you exactly which one to run on your OS.

port-8080-diagnose.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# What the failure looks like in the log
./mvnw spring-boot:run
# ... Spring Boot banner ...
# Web server failed to start. Port 8080 was already in use.
# Caused by: java.net.BindException: Address already in use

# Prove who owns the port (macOS and Linux)
lsof -i :8080
# COMMAND   PID   USER   FD   TYPE  NODE NAME
# java    48213  dev    51u  IPv6  ...  TCP *:http-alt (LISTEN)

# Same check with ss (Linux, no lsof needed)
ss -ltnp 'sport = :8080'
📊 Production Insight
In the Friday-deploy incident, the team rebuilt twice before anyone ran ss. The log line naming port 8080 was visible from the first failure. Rule: the port number in the message is the diagnosis — query the OS about that port before touching the build.
🎯 Key Takeaway
Read the Caused by BindException line, ignore the bean traces above it, and treat the error as environment state — then go find the holder.

Finding the Holder: lsof and ss Commands That Name the Process

Your first move is always the same: ask the OS which process owns the listening socket on 8080. On macOS and most Linux desktops, lsof -i :8080 prints the COMMAND, PID, and USER — everything you need. The -sTCP:LISTEN variant filters out transient client connections so you see only the true holder. When you need just the number for a script, lsof -ti:8080 prints the bare PID, which pipes straight into kill.

On servers and minimal containers where lsof isn't installed, ss -ltnp 'sport = :8080' is your tool. The -l shows listening sockets, -t restricts to TCP, -n skips slow DNS lookups, and -p reveals the owning process in the users field. It runs instantly even with thousands of connections, which matters on busy hosts where lsof crawls. Older boxes may still have netstat; the grep form works but parses less reliably, so prefer ss wherever it exists.

Read the output before acting. A COMMAND of java with your project's path means your own previous run — safe to kill. Anything else (a proxy, a media server, a colleague's tool) means you should move your app instead of killing theirs. This thirty-second read prevents the classic blunder of kill -9ing someone else's debug session.

find-port-holder.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# macOS and most Linux desktops
lsof -i :8080
lsof -iTCP:8080 -sTCP:LISTEN

# PID only — perfect for scripting
lsof -ti:8080

# Linux servers and containers (procps / iproute2)
ss -ltnp 'sport = :8080'
netstat -ltnp 2>/dev/null | grep ':8080'

# Windows PowerShell equivalent
# netstat -ano | findstr :8080
📊 Production Insight
In the Friday-deploy incident the holder turned out to be a hand-started debug JVM, exactly the case where reading COMMAND first matters. Rule: java with your own project path means a stale run you can kill; anything else means move your app instead of killing theirs.
🎯 Key Takeaway
lsof -i :8080 on desktop, ss -ltnp on servers; read COMMAND before you kill anything.

Killing It Safely: Stop, Kill, and Verify the Port Is Free

Killing is a two-step escalation, not a single hammer blow. Start with a plain kill <PID> (SIGTERM): Spring Boot catches it, runs shutdown hooks, closes the context gracefully, and releases the socket. Wait about five seconds, then re-run your lsof or ss check. An empty result means the port is free and you can relaunch with confidence.

If the process survives — common with a JVM stuck in a shutdown hook or a debugger holding threads — escalate to kill -9 <PID> (SIGKILL). The kernel reaps it immediately with no cleanup, so in-flight requests die and temp files may linger. That's acceptable for a local dev process but never your first choice on shared infrastructure. After a SIGKILL, always re-verify: sockets can sit in TIME_WAIT briefly, and relaunching a half-second too early reproduces the exact error you just fixed.

The lsof -ti:8080 | xargs kill -9 one-liner is popular and dangerous in equal measure: it kills whatever holds the port without asking. Use it only on your own laptop where you're certain the holder is your stale JVM. On any shared box, name the PID explicitly so the command documents your intent and your shell history shows what you actually killed. A few extra seconds of verification beats another failed launch and another round of head-scratching.

free-port-8080.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. Polite stop — lets shutdown hooks and Spring's
#    graceful shutdown finish in-flight work
kill 48213
sleep 5
lsof -i :8080

# 2. Still there? Force it.
kill -9 48213
sleep 2
lsof -i :8080
# (empty output = port is free)

# 3. One-liner for your own stale JVM (careful!)
lsof -ti:8080 | xargs kill -9

# 4. Verify with a fresh bind before relaunching
#    (python3 is just a quick socket probe)
python3 -c "import socket; s=socket.socket(); s.bind(('0.0.0.0',8080)); print('port 8080 is free')"
📊 Production Insight
The incident engineer used SIGTERM first and the stray debug JVM exited cleanly in two seconds, so SIGKILL was never needed. Rule: polite kill plus a re-check closes most cases; escalate only when the process survives, and never relaunch before the port reads empty.
🎯 Key Takeaway
SIGTERM first, verify, then SIGKILL only if needed — and re-check the port before relaunching.

Moving Your App: server.port, SERVER_PORT, and Profiles

Sometimes killing isn't an option — the holder is a teammate's service or a tool you need. Moving your app takes one line: server.port=8081 in application.properties. Spring Boot reads it at startup and the embedded container binds the new port instead. The startup log confirms with Tomcat started on port 8081, which is the proof you should always look for rather than assuming the edit took effect.

But properties are only one layer. The full precedence chain is: command-line arguments (--server.port=8083) win over the SERVER_PORT env var, which wins over profile-specific files like application-dev.properties, which win over plain application.properties. When your edit looks ignored, something higher in the chain is overriding it — a stale export SERVER_PORT=8080 in your shell profile is the usual suspect, and echo $SERVER_PORT exposes it in one second.

For teams, make ports explicit per service: 8081 for the API, 8082 for auth, and so on, recorded in the README. Env vars are the cleanest vehicle for this because they vary per machine without touching committed files. Containers and systemd units should each set SERVER_PORT explicitly so no service ever depends on the 8080 default surviving contact with reality.

application.propertiesJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# src/main/resources/application.properties
server.port=8081

# Profile-specific override (active only with -Dspring.profiles.active=dev)
# src/main/resources/application-dev.properties
# server.port=8082

# Command line beats everything in the files
# ./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8083

# Env var beats properties files (great for CI and containers)
# export SERVER_PORT=8084

# Confirm which port actually bound — look for this line:
# Tomcat started on port 8081 (http) with context path ''
⚠ Port Settings Have a Pecking Order
Command-line flags beat env vars, which beat profile files, which beat application.properties. When your port setting looks ignored, walk that chain from the top — the winner is usually a forgotten SERVER_PORT export.
📊 Production Insight
The deploy script now sets SERVER_PORT explicitly per service, so no release depends on the 8080 default surviving. Rule: env-var assignment in the deploy unit beats tribal knowledge about which ports are free.
🎯 Key Takeaway
server.port moves your app; when it's ignored, walk the override chain — CLI, env var, profile file, base file.

Tests That Stop Fighting: RANDOM_PORT and Parallel Builds

Tests are the stealthiest source of bind conflicts. A suite annotated with DEFINED_PORT (or the default MockEnvironment-free setup that still binds 8080) works fine alone but explodes the moment your build runs forks in parallel: two JVMs race for the same fixed port, one wins, and the loser fails with the familiar BindException. On CI the failure looks flaky because which fork wins varies run to run.

The fix is `WebEnvironment.RANDOM_PORT`: Spring Boot asks the OS for a free ephemeral port per test context, so parallel forks never collide by construction. You read the assigned port with @LocalServerPort and build request URLs from it instead of hard-coding localhost:8080. This keeps every assertion identical while removing the shared mutable state — the fixed port — that caused the race.

Reserve DEFINED_PORT for the rare test that genuinely needs a stable URL, like a contract test asserting on callback addresses, and mark those tests so they never run in parallel with anything. For everything else, make RANDOM_PORT the team default and enforce it in review: any new test class binding a fixed port should have to justify itself. Flaky-port CI failures are a tax you can legislate away in one pull request.

OrderApiTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class OrderApiTest {

    @LocalServerPort
    private int port; // injected: the actual free port chosen

    // Build URLs from the injected port — never hard-code 8080
    private String baseUrl() {
        return "http://localhost:" + port;
    }
}
📊 Production Insight
After the incident the team also found two integration tests on DEFINED_PORT flaking in parallel CI. Rule: RANDOM_PORT is the default; a fixed test port must justify itself in review.
🎯 Key Takeaway
RANDOM_PORT plus @LocalServerPort removes the shared fixed port that makes parallel test builds flaky.

Docker and Lingering Runs: Publish Conflicts and Zombie JVMs

Docker adds a layer of misdirection: each container has its own network namespace, so 8080 inside two containers is perfectly legal — the clash happens at the host publish step. When two services both declare -p 8080:8080 or the same compose ports entry, the second start fails with port is already allocated. Spring Boot never even launches, which sends you hunting JVM causes for a Docker problem.

Diagnose with docker ps --format '{{.Names}} {{.Ports}}': it lists every container's host mapping in one glance, and the duplicate 0.0.0.0:8080 jumps out immediately. The fix is a distinct host port per service — -p 8081:8080 keeps the container's internal 8080 untouched while exposing it safely. In compose files, audit the ports section the same way; host ports must be unique even when container ports repeat.

The twin trap is the lingering local run: you containerized the app but the old ./mvnw spring-boot:run JVM is still alive on the host's 8080, so the container's publish fails. When docker ps shows no duplicate yet the publish still fails, fall back to lsof on the host — the holder is a zombie JVM from before you adopted Docker, and one kill closes the case. Make the host check part of your container debugging habit and publish conflicts will stop surprising you.

docker-port-fix.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Who is actually publishing the host's 8080?
docker ps --format '{{.Names}} {{.Ports}}'
# shop-api   0.0.0.0:8080->8080/tcp
# auth-api   0.0.0.0:8080->8080/tcp  <-- clash (second start fails)

# Fix: give the second service its own host port
docker run -d --name auth-api -p 8081:8080 auth-image

# docker-compose.yml — distinct host ports per service
# services:
#   shop-api:
#     ports: ["8080:8080"]
#   auth-api:
#     ports: ["8081:8080"]

# Also check: is it even Docker? A zombie JVM outside
# containers can hold 8080 while you blame compose.
lsof -i :8080 || ss -ltnp 'sport = :8080'
📊 Production Insight
The staging box ran both a host JVM habit and new containers, which is why docker ps alone misled the team at first. Rule: when the publish still fails with no duplicate container, check the host with lsof before blaming compose.
🎯 Key Takeaway
Container ports may repeat; host publishes must not — audit with docker ps, then check for zombie host JVMs.
● Production incidentPOST-MORTEMseverity: high

The Friday Deploy That Failed Three Times Over One Forgotten Debug Session

Symptom
The staging deploy died seconds after the Spring banner with Web server failed to start. Port 8080 was already in use. CPU and memory were normal, the artifact checksum matched the last good build, and rolling back changed nothing — every attempt failed identically.
Assumption
The team assumed the staging deploy had failed to start because of a bad build, so they rebuilt and redeployed twice. Each attempt died faster than the last. Nobody checked the port, because port conflicts were considered a 'local laptop problem' that couldn't happen on a server.
Root cause
That morning an engineer had SSHed into the staging box and started the service by hand on port 8080 to test a config tweak, then closed the laptop without stopping it. The afternoon deploy tried to bind the same port and the OS refused. The CI logs showed only the BindException, so the team chased the artifact instead of the environment.
Fix
The on-call engineer ran ss -ltnp 'sport = :8080', found PID 2141 — a manual debug session started over SSH that morning and left running. They killed it, the deploy started cleanly, and then they made three permanent changes: every service got a distinct SERVER_PORT in its systemd unit, the deploy script got a pre-flight port check that fails fast with the holder's PID, and ad-hoc SSH runs were pointed at 18080+ so they can never squat on a production port again.
Key lesson
  • A bind failure is an environment fact, not a code defect — check the port before you rebuild. Two wasted redeploys cost this team 40 minutes that one ss command would have saved.
  • Every long-lived port assignment should live in exactly one managed place (systemd unit, compose file, or manifest). A human starting a process by hand bypasses all of it.
  • Pre-flight checks beat post-mortems: a five-second port probe in the deploy script turns a 40-minute outage into a loud, instant, self-explaining failure.
Production debug guideFive confirmed fixes for the five ways port 8080 ends up occupied — each with the exact command to run.5 entries
Symptom · 01
App fails at startup with Port 8080 was already in use
→
Fix
Run lsof -i :8080 and read the COMMAND and PID columns. If COMMAND is java, it's likely your own previous run — note the PID and move to item 2. If it's something else (a proxy, media server, another tool), decide whether to stop it or move your app to 8081 instead.
Symptom · 02
You found the PID but need the port freed safely
→
Fix
Run kill <PID>, wait five seconds, then run lsof -i :8080 again. Empty output means you're clear — relaunch. If the process survives, escalate to kill -9 <PID> and re-verify. Never skip the re-check: a half-dead JVM can still hold the socket briefly.
Symptom · 03
lsof is not installed in this environment
→
Fix
Run ss -ltnp 'sport = :8080' when lsof isn't available (minimal containers, some Linux distros). The output names the owning process and PID in the users field. Then kill that PID the same way and confirm with a second ss run.
Symptom · 04
Spring Boot runs in Docker and the port looks busy anyway
→
Fix
Run docker ps --format '{{.Names}} {{.Ports}}' and look for two lines claiming 0.0.0.0:8080. Stop the extra container with docker stop <name>, or republish with -p 8081:8080. Re-run docker ps to prove the clash is gone before restarting your app.
Symptom · 05
You need the app up now and can't kill the holder
→
Fix
Run SERVER_PORT=8081 ./mvnw spring-boot:run (or set server.port=8081) to sidestep the conflict entirely. Watch the startup log for the line Tomcat started on port 8081 — that line is your proof the override took effect.
Port 8080 Bind Failures Compared
Root CauseHow to ConfirmFixPrevention
Lingering previous run still owns 8080lsof -i :8080 shows a java process you started earlierkill <PID> and confirm the port is clear before relaunchingAlways stop the old run first; check the IDE console for a live process
Unrelated tool occupies 8080lsof -i :8080 shows a non-Java COMMAND like a proxy or media serverStop that tool or move Spring Boot with server.port=8081Reserve 8080 for your app; document local port assignments per project
Docker host-port publish clashdocker ps shows two containers mapping to 0.0.0.0:8080Republish one container with -p 8081:8080Give each service its own host port; review compose port maps in CI
Parallel tests binding a fixed portFailure only under parallel or CI builds with DEFINED_PORTSwitch tests to WebEnvironment.RANDOM_PORTDefault all new tests to RANDOM_PORT; forbid DEFINED_PORT in reviews
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
port-8080-diagnose.sh./mvnw spring-boot:runReading the BindException
find-port-holder.shlsof -i :8080Finding the Holder
free-port-8080.shkill 48213Killing It Safely
application.propertiesserver.port=8081Moving Your App
OrderApiTest.java@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)Tests That Stop Fighting
docker-port-fix.shdocker ps --format '{{.Names}} {{.Ports}}'Docker and Lingering Runs

Key takeaways

1
Port 8080 in use means another socket owns the port
your code is fine, so find the holder before changing anything.
2
lsof -i :8080 or ss -ltnp 'sport = :8080' names the PID; kill politely first and escalate to kill -9 only if needed.
3
server.port moves your app; SERVER_PORT env var beats properties files, so check overrides when a setting looks ignored.
4
Tests should default to RANDOM_PORT with @LocalServerPort
fixed ports turn parallel CI builds into coin flips.
5
Docker containers each get their own 8080, but host publishes must be unique
map -p 8081:8080 for the second service.
6
Stop the old run before starting a new one; most bind errors are just yesterday's JVM still holding the port.

Common mistakes to avoid

5 patterns
×

Killing a random PID without checking what holds the port

Symptom
You kill a teammate's debug session, a running database console, or a system service. Your app starts, but something else in the environment breaks and nobody can trace it back to your kill -9.
Fix
Run lsof -i :8080 to identify the holder, then decide: stop it if it's yours, or move your app with server.port=8081. Never kill a PID you haven't identified — check the COMMAND column first.
×

Setting server.port in the wrong file or profile

Symptom
You edit application.properties but the app still binds 8080 because an active profile's application-dev.properties or an env var overrides it. The change looks saved but has zero effect at runtime.
Fix
Put server.port in the right place: application.properties at src/main/resources, or pass --server.port=8081 on the command line, or export SERVER_PORT=8081. Verify with the startup log line showing the new port.
×

Starting a second instance while the first run is still alive

Symptom
Every restart from the IDE fails with the bind error even though you 'stopped' the app. A zombie JVM from an earlier run still owns 8080, and each new launch attempt stacks another error on top.
Fix
Stop the old run before starting a new one: use your IDE's stop button and confirm the JVM exits, or kill lingering java processes. Better yet, run each service on its own port during local development.
×

Publishing every container's 8080 to the host's 8080

Symptom
The second docker run fails with 'port is already allocated' while Spring Boot itself looks fine. Each container thinks it owns 8080, but the host can hand that port out only once.
Fix
Map a different host port per service, e.g. -p 8081:8080, and keep a local port registry in the README. Check clashes up front with docker ps --format '{{.Names}} {{.Ports}}'.
×

Using DEFINED_PORT in tests that run in parallel builds

Symptom
Tests pass locally but fail on CI with bind errors whenever surefire forks overlap. Two test JVMs race for 8080, one wins, the other dies — and the failure looks random from build to build.
Fix
Annotate tests with @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) and inject the port with @LocalServerPort. Reserve DEFINED_PORT for the rare test that asserts on a fixed URL.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does 'Port 8080 was already in use' actually mean?
Q02JUNIOR
How do lsof and ss differ when finding the port holder?
Q03SENIOR
Why would server.port in application.properties be ignored?
Q04SENIOR
When should tests use RANDOM_PORT instead of DEFINED_PORT?
Q05SENIOR
Why do two containers clash when each has its own 8080?
Q01 of 05JUNIOR

What does 'Port 8080 was already in use' actually mean?

ANSWER
It means the embedded Tomcat (or Jetty/Undertow) tried to bind TCP port 8080 but the OS refused because another socket already listens there. You confirm the holder with lsof -i :8080, then either stop that process or move your app with server.port.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I just run my app on a different port permanently?
02
Is kill -9 safe for a stuck Spring Boot process?
03
Why does the OS refuse to share the port between two apps?
04
Can two Docker containers both use port 8080?
05
How do I use a different port locally than in staging?
06
My tests pass alone but fail together — what's the fix?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Spring. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Java Could Not Create JVM Fix
4 / 4 · Spring
Next
Android dexBuilderDebug Failed Fix
→