| name | macos-launchctl-port-conflict-resolution |
| description | Diagnose and resolve TCP port conflicts on macOS when launchctl-managed services compete for the same port. Covers process identification, plist editing, zombie-process killing, and clean restart. Trigger keywords: port conflict, EADDRINUSE, address already in use, launchctl service, port already in use, zombie process, lsof port. |
| license | MIT |
| metadata | {"version":"1.1.0","hermes":{"tags":["macOS","launchctl","Ports","Diagnostic","Services"],"related_skills":["postgresql-nix-macos-init"]}} |
macOS launchctl port conflict resolution
Resolve "port already in use" / EADDRINUSE errors between launchctl-managed services on macOS.
Symptoms
EADDRINUSE: address already in use :::3000
- Service fails to start;
lsof shows multiple PIDs on the same port
- Service works intermittently; reconnects in loops with "stream errored out"
- After changing plist port, old process still listening on old port (zombie)
Workflow
Step 1 — diagnose
lsof -i :<PORT> -P
lsof -i TCP -P | grep LISTEN
ps -fp <PID>
launchctl list | grep <service-name>
Look for: multiple PIDs claiming the same port, OR one PID still on the old port after a plist change.
Step 2 — locate the plist
find ~/Library/LaunchAgents -name "*<service-name>*" 2>/dev/null
find /Library/LaunchAgents -name "*<service-name>*" 2>/dev/null
User LaunchAgents are most common. System Integrity Protection blocks /System/Library/... plists even with sudo — use user agents instead.
Step 3 — edit the port
Common port specifications inside the plist:
--port 3000 in ProgramArguments array
PORT=3000 in EnvironmentVariables dict
- Hardcoded inside the executed script
Change <string>3000</string> → <string>9876</string> in whichever location applies. Watch for ports specified in MULTIPLE places — change all of them.
Step 4 — kill all instances (critical — prevents zombie loops)
Common mistake: only killing the new PID, leaving the old zombie fighting it for the port.
kill -9 <OLD_PID> <NEW_PID> 2>/dev/null
pkill -9 -f "<service-pattern>"
launchctl stop alone does NOT kill orphaned processes — kill -9 on all matching PIDs is required.
Step 5 — restart cleanly
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/<service>.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/<service>.plist
launchctl unload ~/Library/LaunchAgents/<service>.plist
launchctl load ~/Library/LaunchAgents/<service>.plist
A full unload/bootout + load/bootstrap cycle is required to pick up plist changes — stop+start (and kickstart -k) won't re-read the plist.
Step 6 — verify
lsof -i :<NEW_PORT> -P
launchctl list | grep <service-name>
curl -s http://localhost:<NEW_PORT>/health 2>/dev/null
Verification checklist:
Common scenarios
Example: a background bridge service vs a web app
lsof -i :3000
ls ~/Library/LaunchAgents/com.example.bridge-service.plist
sed -i 's/3000/9876/g' ~/Library/LaunchAgents/com.example.bridge-service.plist
pkill -9 -f "bridge-service"
launchctl unload ~/Library/LaunchAgents/com.example.bridge-service.plist
launchctl load ~/Library/LaunchAgents/com.example.bridge-service.plist
curl http://localhost:9876/health
Multiple PostgreSQL instances
lsof -i :5432
psql -U $(whoami) -d postgres
kill -9 $(lsof -ti:5432)
Dev server on a standard port (Vite, Next.js, Rails, etc.)
Three options, in order of preference:
- Change the dev server port (CLI arg or env var).
- Stop the conflicting launchctl service temporarily.
- Reconfigure the launchctl service permanently (this skill).
Pitfalls
launchctl stop is not enough — must unload then load to pick up plist changes.
KeepAlive=true services auto-restart — without changing the plist, the old port keeps getting reclaimed.
- Ports specified in multiple places — some services have it in both ProgramArguments AND EnvironmentVariables; change both.
- Zombie processes after plist edit —
lsof showing - (no process) for old port when edit was incomplete; use pkill -9 -f for pattern cleanup.
- System Integrity Protection —
/System/Library/... plists can't be modified even with sudo; recreate as a user agent in ~/Library/LaunchAgents/.
Management commands
launchctl list
launchctl list | grep -v "^-"
launchctl start com.example.service
launchctl stop com.example.service
launchctl unload ~/Library/LaunchAgents/com.example.service.plist
launchctl load ~/Library/LaunchAgents/com.example.service.plist
launchctl kickstart -k gui/$(id -u)/com.example.service
tail -f ~/Library/Logs/com.example.service.log