'Address Already in Use' on Mac: Fix EADDRINUSE and Free the Port
Address already in use (EADDRINUSE) means another process holds the port. Here's how to find it on macOS, free the port, and stop it recurring.
You start a server and it dies immediately with bind: address already in use (or Address already in use, errno 48 on macOS). Node calls the same failure EADDRINUSE, Docker phrases it as “port 3000 is already allocated,” and you’ll also see it worded simply as “port already in use,” they’re all the same problem. The message is blunt but accurate: something already owns the port you’re trying to bind to. Here’s how to find it and get your port back.
What the error actually means
When a program wants to listen on a port, it calls bind() on that port number. If another socket is already bound there, the OS refuses with EADDRINUSE, “address already in use.” On macOS this is errno 48 (it’s 98 on Linux, in case you’re cross-referencing).
There are two distinct causes, and the fix differs:
- Another process is genuinely holding the port (the common case).
- A previous instance of your own program left the port in
TIME_WAIT(the sneaky case).
If you’re seeing EADDRINUSE from Node.js
Node’s version of this error looks like:
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:...)
Same failure, more detail. When your code calls app.listen(3000), it asks the OS to bind to port 3000, and if something already holds that port, the bind fails and Node surfaces EADDRINUSE. The :::3000 in the message is just the IPv6 form of “port 3000 on all interfaces.” Nine times out of ten in development, the culprit is a previous instance of your own server that didn’t shut down: a crashed process, a stuck nodemon, or a terminal you closed without stopping it. The fix is the same as the general case below.
Step 1: Find what’s on the port
Replace 3000 with your port:
sudo lsof -i :3000 -n -P
If something owns it, you’ll see the process and PID:
COMMAND PID USER ... NODE NAME
node 1421 aaron ... TCP *:3000 (LISTEN)
Now you know node, PID 1421, is holding port 3000. If the process name doesn’t ring a bell, see which app is using a port on Mac for how to track down what it actually is.
Step 2: Free the port
If that process is safe to stop (an old dev server, a forgotten script), kill it:
# Graceful first, lets it clean up
kill 1421
# If it won't go, force it
kill -9 1421
Or do it in one shot without copying the PID:
kill -9 $(lsof -ti :3000)
The -t flag makes lsof print just the PID, which feeds straight into kill. See kill a process by port on Mac for when to prefer the graceful SIGTERM over the forceful SIGKILL. Once it’s dead, confirm the port actually cleared:
lsof -i :3000 -n -P
An empty result means it’s free. If something’s still there, you didn’t kill what you thought you did.
The TIME_WAIT case: nothing is on the port, but it’s still “in use”
Sometimes lsof -i :3000 returns nothing, yet you still get “address already in use.” This is usually TIME_WAIT: when a TCP connection closes, the OS keeps the socket reserved for a short cooldown (typically around 30 seconds on macOS) to make sure no stray packets from the old connection get misdelivered to a new one.
You can confirm it:
netstat -an | grep 3000
If you see the port in TIME_WAIT rather than LISTEN, that’s what’s blocking the rebind. Two ways through it:
- Just wait. The state clears on its own, usually within about 30 seconds on macOS.
- Make your server reuse the address. Most servers can set the
SO_REUSEADDRsocket option, which lets a new socket bind to a port still inTIME_WAIT. In Node it’s on by default; in many frameworks there’s a “reuse address” flag. This is the right long-term fix for servers you restart constantly.
Step 3: Or just change your port
If you can’t safely kill what’s holding the port, point your app somewhere else instead:
# Node
PORT=3001 npm start
# Vite (in vite.config or CLI)
npm run dev -- --port 5174
# Flask
flask run --port 5001
# Rails
rails server -p 3001
This works cleanly when your code reads the port from the environment, which it should:
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on ${port}`));
Moving your app is often safer than killing a process you can’t immediately identify, especially if the port belongs to a system service.
Stop it from recurring
A few habits eliminate most repeat offenders, particularly on Node:
Handle the error instead of crashing blind. Catch it and print something useful:
const server = app.listen(port);
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use. Stop the other process or set a different PORT.`);
process.exit(1);
} else {
throw err;
}
});
Shut down cleanly on Ctrl-C so the port is released when you stop the server:
process.on('SIGINT', () => {
server.close(() => process.exit(0));
});
Use a process manager like nodemon or pm2 that restarts your app and releases the old port between runs, instead of leaving orphans behind.
Why this keeps happening
The root cause is always the same: only one program can listen on a port at a time. Crashed servers that didn’t release their port, two tools both defaulting to 3000, a debugger still attached in the background, all produce the same error. The skill is quickly identifying what holds the port, which is exactly what the steps above do.
The common offenders
Most “address already in use” errors trace back to a short list of usual suspects:
- 3000 → Node, React, Rails (port 3000 already in use)
- 5000 → Flask, and AirPlay Receiver on macOS (port 5000 in use)
- 8080 → Tomcat, proxies, second web apps (what is port 8080)
- 5173 → Vite
- 5432 → PostgreSQL
One macOS-specific gotcha: port 5000 and 7000 are often held by AirPlay Receiver, not a program you started. If you can’t find your process on 5000, that’s usually why, turn off AirPlay Receiver in System Settings or just use a different port.
Find the culprit instantly
Portie shows every process holding a port on your Mac in one live table, so when you hit “address already in use,” you can see exactly what’s on that port without typing an lsof command, and end it with one click.
Local monitoring is free. The $8.99 one-time unlock adds one-click process termination (graceful or forced) and remote port scanning. Download Portie and never decode errno 48 by hand again.