Kay van Dijk
Who has that port
Every few weeks something will not start because the port is taken. A dev server, a container, Home Assistant, whatever I was about to run. I know the answer is lsof or ss. I also know I will get the flags wrong, look them up, and forget them again. busy-port is the command I actually wanted to type: a port number, and the process that has it.
It is a small Python program. 3.10 or newer, standard library only, so a checkout can run it without a pile of dependencies: python -m busy_port 8080. Installed, the command is busy-port 8080, or busy-port --port 8080 if I feel like being explicit. The default protocol is TCP. --proto udp or --proto both if I need the other one. DNS on 53 is the usual reason I ask for UDP.
What it prints
When something is listening, a text line looks like this: tcp 8080 LISTEN pid=1234 cmd=python. That is the example from the README, with a fake pid, so you can see the shape. --json prints a list of objects with port, proto, state, pid, and cmd. When the port is free, the text is "nothing is listening on tcp port 8080". With --json and a free port, stdout is an empty list and the same message goes to stderr, so a script can tell "empty result" from "the tool complained."
Exit codes are boring on purpose. 0 means it found at least one listener. 1 means nothing is listening. 2 means bad arguments, or the lookup could not run. I did not want a tool that exits 0 when it failed to look.
Where the numbers come from
On Linux it reads /proc/net/tcp, /proc/net/tcp6, /proc/net/udp, and /proc/net/udp6. It maps the socket inode to a PID by scanning /proc/*/fd. That works without root for processes you own. If those files are missing, it falls back to ss -lptn and ss -lpun. I would rather read proc than remember ss flags, and I would rather fall back than pretend the port is free.
On macOS it runs lsof. For TCP that is lsof -nP with the port and -sTCP:LISTEN. For UDP it is lsof -nP on that UDP port. -nP so it does not sit there resolving names.
Windows is the limited one, and the README says so. It runs netstat -ano and, when that returns a PID, tasklist. It expects English netstat output. Some sockets still hide the process name. I did not build a clever parser for every localized netstat. If you are on a Dutch Windows, this path may not read the table. I would rather say that than ship a guess.
It does not invent a PID
If the socket is visible but the PID is not, the line uses pid=? and cmd=?. On Linux that usually means another user owns the process. Run it as root if you need to read their /proc entries. I was tempted to print "unknown process" as if that were an answer. It is not. A question mark is the honest output.
That is the whole tool. No daemon, no config file, no account. It answers one question I kept asking the wrong command. The repo is github.com/kaygaaf/busy-port. Tests are pytest, from a checkout with the dev extra. If you only want to try it, python -m busy_port and a port number is enough.
