Port Already in Use on macOS: How to Fix EADDRINUSE

Find what is using a port on macOS, identify the right process, and fix EADDRINUSE errors in development servers, SSH tunnels, and parallel worktree sessions.

To investigate a port already in use on macOS, run lsof -nP -iTCP:3000 -sTCP:LISTEN, replacing 3000 with the port in the error. Identify the process and project before stopping it. If the existing server is doing useful work, give the new server a different port instead.

This guide covers Node's EADDRINUSE, development servers that keep reappearing, and SSH tunnels that cannot bind a local port. You can follow it in Terminal.app or another macOS terminal. Only the small reproduction below requires Node.js.

Understand which address could not be bound

A typical Node error looks like this:

Error: listen EADDRINUSE: address already in use 127.0.0.1:3000

It says the program could not bind that local address because it was already occupied. This is different from ECONNREFUSED, where a client tried to connect and the target refused the connection. Node documents both in its system-error reference.

Record the address and port before changing anything. 127.0.0.1:3000, 0.0.0.0:3000, and [::]:3000 describe different bind requests. The same port number on a remote machine is also distinct from that port on your Mac.

Find the process listening on the port

For TCP port 3000:

lsof -nP -iTCP:3000 -sTCP:LISTEN

An illustrative result is:

COMMAND   PID   USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
node    41234  alice   21u  IPv4     ...      0t0  TCP 127.0.0.1:3000 (LISTEN)

The PID identifies the process. COMMAND is a short executable name, not necessarily the project name. NAME shows the listening address. A browser merely connected to port 3000 should not be mistaken for the listener.

The options keep that distinction clear: -n avoids hostname lookups, -P preserves numeric ports, -iTCP:3000 selects the TCP port, and -sTCP:LISTEN filters for listeners. See the lsof manual.

No output means this invocation found no visible matching listener at that moment. If lsof prints a permissions error, that is not an empty result. If the error repeats, check the exact port, address family, and machine where the server is starting.

Identify the project before stopping anything

Replace 41234 in these examples with the PID you just found:

ps -p 41234 -o pid=,ppid=,user=,comm=
lsof -a -p 41234 -d cwd

The first command shows the process and its parent PID. The second shows its working directory. The -a matters: it combines the PID and directory selections, so you inspect that process rather than a broader set of open files.

Suppose the working directory is /Users/alice/projects/storefront-preview. You now know this is the preview you left running, not necessarily a stray process from the project in your current terminal.

If a watcher or editor launched it, inspect the parent PID with the same ps command. The process that owns the listening socket and the process that decides to restart the server can be different. Use the working directory and terminal session name to find the appropriate stop control.

Stop the server through its owner, then verify

For a foreground development server, return to its terminal and press Ctrl+C. For an editor-managed task, use the editor's stop action. For a container or another managed service, use that service's normal stop control.

If you have confirmed that a standalone process is yours and should stop, request termination by PID:

kill -TERM 41234

Give it a moment to shut down, then repeat the original listener check:

lsof -nP -iTCP:3000 -sTCP:LISTEN

Now restart your intended application and read the address it reports. If the same port immediately gains a new PID, investigate the watcher or service manager restarting it. Repeatedly terminating the child does not change that supervisor's instructions.

Force termination removes the opportunity for normal cleanup. Reserve it for a confirmed, unresponsive process after a normal stop fails. Apple's Activity Monitor guide explains the same distinction between quitting and force quitting.

Avoid broad commands that terminate every Node process. Another project's dev server, a language server, or a local agent integration may use the same executable.

Try the workflow with a small server

This example creates an HTTP server bound only to your Mac's IPv4 loopback address. Save it as port-demo.cjs in a scratch directory:

const http = require('node:http');

const port = Number(process.argv[2] ?? 3000);
if (!Number.isInteger(port) || port < 0 || port > 65535) {
  console.error('Pass an integer port from 0 through 65535.');
  process.exit(2);
}

const server = http.createServer((request, response) => {
  response.writeHead(200, { 'Content-Type': 'text/plain' });
  response.end('port-demo is running\n');
});

server.on('error', (error) => {
  console.error(`${error.code}: ${error.message}`);
  process.exitCode = 1;
});

server.listen(port, '127.0.0.1', () => {
  console.log(`PID ${process.pid}: http://127.0.0.1:${server.address().port}`);
});

function stop() {
  server.close(() => process.exit(0));
}
process.on('SIGINT', stop);
process.on('SIGTERM', stop);

Check that port 3000 is available, then start the demo:

node port-demo.cjs 3000

In a second terminal, run the same command from the same scratch directory. The second process should report EADDRINUSE, while the first continues running. Use lsof to identify the first process, then request its small test page:

curl --silent --show-error --max-time 5 http://127.0.0.1:3000/

The expected body is port-demo is running. Stop the first server with Ctrl+C, then retry the second command. It should now start successfully. Stop that copy when you finish.

If port 3000 belongs to something you need, use another available port throughout the exercise. This reproduction lets you practise the diagnosis without interfering with an existing project.

Run multiple worktrees on different ports

Two Git worktrees have separate working files, but they still run on the same Mac. A server in each worktree cannot normally listen on the same address and TCP port at the same time.

Use a small allocation table in your task notes:

Session Frontend URL Backend URL
Main branch http://127.0.0.1:3000 http://127.0.0.1:8000
Checkout change http://127.0.0.1:3001 http://127.0.0.1:8001
Documentation preview http://127.0.0.1:3002 None

These are example assignments; verify that the ports are available. Set each application's port using the option its own development server documents. A PORT environment variable is not universally supported.

Update the frontend's API destination and any port-specific callback or allowed-origin settings together. Otherwise the new frontend may start correctly while still talking to another worktree's backend. Distinct ports also do not isolate databases or other shared state.

For disposable test servers you control, the operating system can allocate a free port. With the demo:

node port-demo.cjs 0

Read the chosen address from the startup message. Node's networking documentation describes port 0 and retrieving the assigned port. Keep that listener open while the test uses it; discovering a free port and closing it first leaves a gap in which another process can claim it.

Diagnose SSH tunnel and container collisions

An SSH local forward needs a listening port on your Mac. If port 3000 is occupied, you can choose 3001 locally while keeping the remote service on 3000:

ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:3001:127.0.0.1:3000 user@dev.example.com

Replace the example host and user with your development server. Visit http://127.0.0.1:3001 locally. The destination 127.0.0.1:3000 is reached from the remote host. ExitOnForwardFailure catches forwarding setup failures; it does not establish that the destination application is healthy. See the OpenSSH manual and our SSH port-forwarding guide.

If Docker is involved, list running containers and their published ports:

docker ps --format 'table {{.Names}}\t{{.Ports}}'

For a mapping such as 127.0.0.1:3000->80/tcp, 3000 is the host port and 80 is inside the container. Change or stop the relevant container through its normal workflow. Stopping an unrelated host process will not change the published mapping. Docker documents this output in its container listing reference.

When the listener still seems to be missing

Check both address families if the error mentions :: or ::1:

lsof -nP -i4TCP:3000 -sTCP:LISTEN
lsof -nP -i6TCP:3000 -sTCP:LISTEN

A wildcard IPv6 listener can also cover IPv4, depending on the operating system and socket options. Node documents the ipv6Only option in its listen options. Changing a client URL from localhost to 127.0.0.1 does not remove an existing server binding.

Also check where the error originated. A command running over SSH must be diagnosed on the remote machine; your Mac's lsof cannot list that machine's processes. A failure on a Unix socket path needs a different investigation from a TCP port collision.

You are finished when the intended process owns the expected address, its test request succeeds, and the other sessions you need still work. Record that URL beside the session name so the next agent or terminal can find it without starting another competing server.

Try Agents UI

A native terminal for AI coding agents with persistent sessions, SSH workflows, and built-in editing.