Whose port is it anyway?
One command to see which process is squatting on a port — and free it if you want. Pure Python standard library: no psutil, no compiled wheels, no Node, no Rust toolchain.
English | 简体中文
Address already in use. EADDRINUSE. bind: Only one usage of each socket address.
You know the drill — and you have googled this exact incantation more times than you'd like to admit:
| Step | Windows | macOS / Linux |
|---|---|---|
| 1. find the PID | netstat -ano | findstr :8080 |
lsof -i :8080 (was it -i:8080? -nP?) |
| 2. find out what it is | tasklist | findstr 23456 |
ps -p 23456 |
| 3. kill it | taskkill /F /PID 23456 |
kill -9 23456 |
Three commands, two syntaxes to remember, and on non-English Windows the netstat output is localized so half the copy-pasted one-liners break anyway.
$ whoseport 8080
PORT PROTO STATE PID PROCESS ADDRESS
8080 tcp LISTEN 31425 node 0.0.0.0
└─ node server.js --port 8080
$ whoseport 8080 --kill
About to terminate 1 process:
PID 31425 node (port 8080)
Proceed? [y/N] y
✓ PID 31425 (node): SIGTERM sent
✓ port 8080: freeThat's it. It shows you who is on the port before anything gets killed.
pip install whoseport # or: pipx install whoseportRun it without installing:
uvx whoseport 8080 # uv
pipx run whoseport 8080 # pipxStraight from GitHub (always works, even before a PyPI release propagates):
pip install git+https://github.com/hc-ui/whoseport
# or grab the wheel from the Releases pageRequires Python ≥ 3.9. Nothing else — the dependency list is empty.
$ whoseport # what's listening on this machine?
$ whoseport 8080 # who is using port 8080?
$ whoseport 3000-3010 5432 # multiple ports and ranges
$ whoseport 8080 --kill # terminate the owner (asks first)
$ whoseport 8080 -k -y -f # no questions, SIGKILL (CI-friendly)
$ whoseport 8080 --wait 60 # block until the port is free (CI, restart loops)
$ whoseport 8080 --json # machine-readable, for scripts and AI agents
$ whoseport 8080 --tcp # TCP only (--udp for UDP only)
$ whoseport 8080 --all # every socket, incl. established connectionsBusy ports stay readable — connection noise is collapsed, listeners stay front and center:
$ whoseport 7897
PORT PROTO STATE PID PROCESS ADDRESS
7897 tcp LISTEN 34040 verge-mihomo.exe 0.0.0.0
7897 tcp6 LISTEN 34040 verge-mihomo.exe [::]
7897 udp - 34040 verge-mihomo.exe 0.0.0.0
+ 165 ESTABLISHED, 77 TIME_WAIT connection(s) from 1 process(es) on the same port(s) - use --all to list them| Code | Meaning |
|---|---|
0 |
found something (or killed it successfully) |
1 |
nothing is using the port(s) |
2 |
usage / platform error |
3 |
kill failed or the port is still busy |
So whoseport 8080 || echo free just works in scripts.
whoseport |
killport (Rust) |
kill-port (npm) |
psutil scripts | raw netstat / lsof |
|
|---|---|---|---|---|---|
| Install | pip / pipx / uvx |
brew / cargo | needs Node | needs a compiled dep | built-in |
| Runtime deps | none (pure stdlib) | none (binary) | Node runtime | psutil wheel | — |
| Shows who before killing | ✓ table + cmdline | dry-run flag | partial | write it yourself | arcane flags |
| Safe by default | ✓ confirm + SIGTERM first | ✗ SIGKILL by default | ✗ kills immediately | up to you | manual |
| Explains TIME_WAIT | ✓ | ✗ | ✗ | ✗ | raw rows |
| Locale-proof on Windows | ✓ native API | ✓ | ✗ parses netstat text | ✓ | ✗ (LISTENING vs 侦听) |
| JSON output | ✓ | ✗ | ✗ | write it yourself | ✗ |
| Verifies the port is actually free after killing | ✓ | ✗ | ✗ | write it yourself | manual |
If you live in the Rust or Node ecosystem, those tools are great. whoseport is for everyone who already has Python — which, if you fight with ports 8000/8888/5000, you probably do.
No psutil. Each platform gets the same treatment the native tools use, minus the parsing hazards:
- Linux — reads
/proc/net/{tcp,tcp6,udp,udp6}and maps socket inodes to PIDs via/proc/*/fd(exactly whatss -pdoes). No subprocess at all. - Windows — calls
GetExtendedTcpTable/GetExtendedUdpTablefromiphlpapi.dllvia ctypes — the same APInetstatitself uses. Locale-proof by construction, no console codepage headaches. Elevated process names are recovered through one batchedtasklistcall. - macOS — uses the
lsofthat ships with macOS, in-Fmachine-readable field mode (not the human table, which shifts between versions).
--kill is designed to be hard to regret:
- Shows exactly what will die and asks for confirmation (skip with
--yes). - Sends
SIGTERMfirst;SIGKILLonly with--force. (On Windows,TerminateProcessis the only reliable option for console apps — equivalent totaskkill /F— and the docs say so instead of pretending otherwise.) - Refuses to touch PID 0/1/4, itself, or its parent shell.
- Re-checks the port afterwards and tells you whether it is actually free, including lingering
TIME_WAITsockets that clear on their own.
--json emits a stable document — handy for shell scripts and for LLM agents that keep hitting EADDRINUSE in dev loops:
{
"whoseport": "0.1.0",
"platform": "win32",
"query": [8080],
"sockets": [
{
"proto": "tcp", "ip_version": 4, "local_addr": "0.0.0.0",
"local_port": 8080, "state": "LISTEN", "pid": 31425,
"process": "node", "cmdline": "node server.js --port 8080"
}
]
}# block until port 8080 is free (exit 0), or give up after 60s (exit 3)
whoseport 8080 --wait 60
# free a port in CI before starting the dev server
whoseport 3000 --kill --yes --force || true
# one-liner check: is the port free right now?
whoseport 8080 >/dev/null || echo "free" # exit 1 = freeThe same engine is importable — still zero dependencies:
import whoseport
for sock in whoseport.collect(ports={8080}):
print(sock.pid, sock.process, sock.state, sock.local_addr)Some rows show ? as PID on Linux.
Sockets of other users are visible, but their owners aren't (same as netstat). Run with sudo to see everything.
A TIME_WAIT row is shown with - as the process.
That socket has no live owner — the kernel keeps it briefly after a connection closes. It clears on its own, and servers that set SO_REUSEADDR can bind right through it. whoseport tells you this instead of letting you hunt for a process that doesn't exist.
On Windows the process name shows but the path doesn't.
The owner runs elevated. Names come from tasklist (works from any terminal); full paths need an elevated terminal.
The port is owned by docker-proxy / com.docker.backend / vpnkit.
Killing that process won't help — stop the container instead: docker ps → docker stop <container>.
PID 4 / System owns port 80 or 443 on Windows.
That's http.sys. Don't kill it — find the reservation with netsh http show servicestate or stop the service using it (often IIS or the "World Wide Web Publishing Service").
git clone https://github.com/hc-ui/whoseport
cd whoseport
pip install -e ".[dev]"
pytest # unit + real end-to-end tests (they bind sockets and kill child processes)
ruff check src testsCI runs the suite on Linux, macOS and Windows across Python 3.9–3.14.