What is using port 5000 on your Mac (and why your tools call it a running server)
Here is what owns port 5000 on my Mac:
$ lsof -nP -iTCP:5000 -sTCP:LISTEN
COMMAND PID USER TYPE NAME
ControlCenter 649 jonas IPv4 *:5000 (LISTEN)
That is macOS itself: AirPlay Receiver, added in Monterey and enabled silently for anyone who upgraded, with Control Center holding the socket. It takes 7000 as well. Of the 156 listening sockets on this machine, those two are the only ones below 10000 owned by a system daemon, and both belong to that one process.
The Flask documentation states it outright, in the “Address already in use” section: “macOS Monterey and later automatically starts a service that uses port 5000.”
It does not just occupy the port. It answers.
$ curl -i http://127.0.0.1:5000/
HTTP/1.1 403 Forbidden
Content-Length: 0
Server: AirTunes/860.7.1
Both ports answer, both with 403, both identifying as AirTunes. That is the part every write-up about this skips, and the part that causes the expensive failures. A TCP connect succeeds. An HTTP request gets a response. Anything built on “can I reach this port” concludes your server is up.
Whether it collides with you depends on one socket option
This surprised me, so I measured it instead of assuming. Same machine,
same daemon on *:5000, four attempts to bind:
| Bind | Result |
|---|---|
127.0.0.1:5000, no SO_REUSEADDR |
OSError: [Errno 48] Address already in use |
127.0.0.1:5000, with SO_REUSEADDR |
binds fine |
0.0.0.0:5000, with SO_REUSEADDR |
Errno 48 |
Ruby TCPServer.new('127.0.0.1', 5000) |
binds fine |
Control Center holds the wildcard address. On BSD, and therefore on
macOS, a socket with SO_REUSEADDR may bind a specific address while
another socket holds the wildcard on the same port. Linux does not allow
that. Ruby’s TCPServer sets SO_REUSEADDR for you; a raw Python socket
does not.
So “does AirPlay break my dev server” has no single answer. It depends on your language’s socket defaults and on whether you bind localhost or all interfaces. It also means a real server and the daemon can be listening on the same port at the same time, which turns out to matter later.
The errors, when you do collide, measured here rather than copied:
| Stack | What it prints |
|---|---|
| Python | OSError: [Errno 48] Address already in use (Linux says 98) |
| Ruby | Errno::EADDRINUSE: Address already in use - bind(2) for "0.0.0.0" port 5000 |
| Node | Error: listen EADDRINUSE: address already in use :::5000 |
| Go | listen tcp 0.0.0.0:5000: bind: address already in use |
Those are the honest failures. The interesting ones print nothing at all.
Four ways my own tool got this wrong
pwt allocates a port per worktree and reports what is running. Pointed at a machine with AirPlay on, every reporting path was wrong:
$ pwt ports
PORT PROJECT WORKTREE STATUS
5000 app feat listening # nothing is running
$ pwt server wait feat --timeout 600
Ready: feat (port 5000) # returned immediately
$ pwt info feat
Server: running (PID 649) # that is Control Center
And the one I am least proud of:
$ pwt remove feat
Error: Processes detected on port 5000:
PID 649 (/System/Library/CoreServices/ControlCenter.app/.../ControlCenter)
Options:
pwt remove feat --kill-port # Kill port processes
The removal was blocked by a process nobody should be killing, and the
suggested way out was kill -9 on Control Center. I found that by
writing the test before the fix: run it against the old code and the
output reads ✓ Port 5000 freed, right after the signal goes out.
pwt server wait is the one that actually costs you. An agent starts a
server, waits for readiness, gets “Ready” instantly, and calls an
endpoint that belongs to AirTunes. The 403 comes back and the failure
surfaces three steps later, somewhere unrelated to the port.
Why one command got it right and the rest did not
pwt already knew how to tell a system daemon from a dev server. The check
existed, correct and complete, in exactly one function, reached by
exactly one command: pwt list -v printed [port 5000: system] while
everything else printed listening.
That is the general shape of this bug and it deserves a name: a filter that lives in one code path is not a filter, it is a coincidence. Each later occupancy check was written by someone looking at the socket, not at the function three files away that had already solved it. No amount of care prevents that. Only a single callable place does.
What “a system process” means, mechanically
You cannot ask the kernel whether a listener is “yours”. You can ask who owns the pid:
port_pid_is_system() {
cmd=$(ps -p "$1" -o command= 2>/dev/null) || return 1
exe="${cmd%% *}"
case "$exe" in /System/* | /usr/libexec/*) return 0 ;; esac
case "${exe##*/}" in
ControlCenter | rapportd | AirPlayXPCHelper | sharingd) return 0 ;;
esac
return 1
}
Two decisions in there carry weight.
A port counts as system only when every listener on it is one. This
is where the SO_REUSEADDR measurement stops being trivia: a Ruby server
really can hold 127.0.0.1:5000 while Control Center holds *:5000.
Verified with both running at once, the port reports listening, which
is correct, because one of the two listeners is a server you started.
Allocation is unchanged. A system-held port stays unavailable for new worktrees, because you cannot reliably bind what Control Center holds. The whole fix is about what gets reported, never about what gets handed out. Making 5000 allocatable again would have been the obvious next step and the wrong one: it hands a worktree a port whose server may fail to start depending on how that server opens its socket.
Cost: one ps over every listener, once per command, 40ms with 151 of
them. Per-port classification instead would be a fork per worktree, which
is exactly what a snapshot exists to avoid.
The options, and what each costs
Turn AirPlay Receiver off. System Settings, General, AirDrop & Handoff. It frees both ports, and you lose the ability to AirPlay to that Mac. If you never use it, do this and stop reading.
Move your port. Correct, and more expensive than it looks. On one
project here the port is not just a flag: it is written into .env, into
the Procfile, and the database name derives from it, so moving the port
meant recreating a database. That is the argument for a tool that
allocates ports and derives the dependent values, instead of copying a
file that already contains a number.
Teach the tool the difference. What I shipped in 0.2.9:
$ pwt ports
PORT PROJECT WORKTREE STATUS
5000 app feat system
system: held by a macOS daemon, not by a server of yours.
AirPlay Receiver takes 5000 and 7000: turn it off in System
Settings > General > AirDrop & Handoff, or move the port with
pwt fix-port <worktree>.
pwt server wait now refuses immediately, with the reason, instead of
waiting out a ten-minute timeout on a port that can never become yours.
One myth, since it is everywhere
Several popular write-ups claim Flask changed its default port to 5001 on
macOS because of this. It did not. The current documentation (3.1.x)
still starts the server on http://localhost:5000/ and tells you to
disable AirPlay Receiver or pass --port 5001 yourself. Check the docs
of whatever you are actually running before trusting a fix you read
somewhere, this article included.
The rule underneath
“Something is bound to this port” and “my server is running” are different questions, and most tools answer the first while printing the second. It stays invisible until a daemon you did not start occupies exactly the port your framework defaults to, which on macOS is the default case.
brew install jonasporto/pwt/pwt, then pwt ports
(reference) to see every
allocation on the machine and who actually holds each one. Ports across
many projects are their own
post,
and per-worktree allocation is
here.
You do not have to memorize any of this.
pwt skillprints the agent-facing guide to pwt; tell your agent the outcome you want and point it there, and it works out the mechanism and verifies it in a throwaway worktree.You do not have to remember which ports macOS takes.
pwt portslabels a port held by a system daemon assysteminstead oflistening, and--jsoncarries"system": truefor scripts. Reference: pwt ports.