Blog

'bind: Address already in use': who really owns that port?

The local port you are asking for is already taken, almost always by a previous SSH port forwarding you thought was closed. An ssh -L interrupted by a closing terminal sometimes leaves its process behind, and that process holds the bind on the address and port. One command names the culprit: lsof -nP -iTCP:5433 -sTCP:LISTEN. The rest of this page explains why it is still there, and how to stop meeting it.

Who is holding the port?

Only one answer is reliable, and the kernel gives it. Here is the failure reproduced: a first tunnel detached with ssh -f -N -L, then a second one asking for the same port.

ssh -N -L 15434:127.0.0.1:8080 server
bind [127.0.0.1]:15434: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 15434
Could not request local forwarding.

And the command that identifies the owner, with its real output:

lsof -nP -iTCP:15434 -sTCP:LISTEN
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ssh     42628 leandre    7u  IPv6 0xb5c650980393c14e      0t0  TCP [::1]:15434 (LISTEN)
ssh     42628 leandre    8u  IPv4 0x5a68cd66228f0eae      0t0  TCP 127.0.0.1:15434 (LISTEN)

Two lines, one process: OpenSSH listens on IPv4 and IPv6 by default, and the same PID holds both. That PID is what you look at before doing anything. If it is an old tunnel of yours, kill 42628 is enough: the process exits cleanly and releases the port. The reflex kill -9, without identifying the process first, is the surest way to stop something other than what you aimed at.

Why does a dead tunnel still hold the port?

Because it is not dead. An ssh -N -L started in a terminal belongs to that terminal, but does not always die with it: detached with -f, started under a multiplexer, or simply spared by how the terminal closes its children, it keeps running with no window to show for it. It consumes nothing, says nothing, and listens on. Days later, the port that "nobody" uses still belongs to it. On macOS in particular, closing a terminal window does not reliably terminate everything that was started inside it, and an ssh launched with -f is designed to outlive it.

The reverse exists too, and the output above shows it: the second ssh does not get the port, but it does not stop either. Could not request local forwarding is not fatal: the SSH session establishes, runs, and the forwarding it was supposed to carry does not exist. You now have two live ssh processes, one holding the port with the old tunnel, one running with no tunnel at all. That combination is what makes this failure so disorienting.

lsof, ss or netstat: the command for your system

SystemThe commandWhat it gives you
macOSlsof -nP -iTCP:5433 -sTCP:LISTENProcess, PID, listening address
Linuxss -tlnp 'sport = :5433'The same, faster, without lsof
Older Linuxnetstat -tlnp | grep 5433The historical form, still everywhere
Bothkill <PID> then reopen the tunnelThe repair, after identification

The lsof flags are not decorative: -nP skips the name lookups that make the command hang, and -sTCP:LISTEN shows only what listens, which is the only thing that can hold a bind. Without it, the output mixes in established connections and drowns the line that matters.

How do you stop meeting this error?

Two habits are enough. First: one port per use, chosen once and written down. Collisions almost always come from improvised ports, 5433 today, 5433 again tomorrow for something else. Second: check before you open, lsof first, ssh -L second, which turns the error into information.

That is exactly what Kestro does on every open: it asks "may I listen here?" before starting the tunnel, and offers a free port instead of failing. The manual method above gives you the same service if you prefer your own aliases, and the guide to tunnels that keep dropping collects the other failures of this family.

What remains

Address already in use has a rarer cousin, Permission denied, which is not about a taken port but a forbidden one: below 1024, only root may listen, and the answer is a higher port, not sudo. There is also the case where lsof shows nothing while the bind still fails: another user of the machine holds the port, and lsof only shows your own processes by default. sudo lsof settles it. And if your tunnel seems open but connections still fail, the message is different and so is the cause: that is the 'connect failed: Connection refused' error.