Processes & Ports: When Things Won't Stop
"Port 3000 is already in use." — every web developer, weekly
Until now every command you ran started, did its job, and finished. But some programs are meant to keep going: dev servers, watchers, builds — and your AI agent starts them constantly. This part is about the programs that are still running: how to see them, how to stop them, and how to fix the single most common error in modern web development, when yesterday's server is still squatting on the port today's server needs.
8.1 Everything Is a Process
Every running program on your machine — the dev server, your editor, the shell you're typing into, the agent itself — is a process: one copy of some code, loaded into memory and kept alive by the operating system. Open three terminal windows and you have three shell processes, each with its own idea of where it's standing. Each one gets a PID — short for process ID — the moment it starts, a number unique on this machine for as long as it lives. That number is the handle you use to do anything to it.
ps on its own lists only the processes attached to this terminal —
usually two or three, and never the dev server you're hunting. Add aux and you get the whole machine:
ps aux
USER PID %CPU %MEM START COMMAND
vibe 1024 0.0 0.1 09:10 bash
vibe 400 0.4 1.8 08:41 node server.js
vibe 437 97.4 0.6 09:07 spinner.sh --foreverThe three letters earn their place: a widens the list to every user's processes, u adds the human-readable columns you're about to read, and x includes the ones with no terminal attached — which is precisely where background
servers hide. (They carry no dash: ps takes its options in an older style that
predates the convention, the same reason tar cf works without one.) Your real
output is wider than this too — macOS and Linux both add memory sizes, a terminal name and a state
column. Ignore those; the ones that matter are here.
USER is the account that owns the process — vibe is you, which
is why you can stop your own dev server and get Operation not permitted on one
of the system's. PID is the number you'll pass to kill. %CPU is how hard it's working, measured against one core, and your machine has several:
separate workers that genuinely run at the same time, so a process using three of them
honestly reads 340%. A runaway of yours sits near 100, and your fans tell you before ps does. %MEM is the share of the machine's memory it's holding, the column to check when
everything goes sluggish rather than hot.
START is the wall-clock time it launched, the one column that separates the server
you started a minute ago from yesterday's, still running. COMMAND is what it actually
is — though that's the interpreter's name, not your project's: a JavaScript server
shows up as node, a Python one as python, and that's what
you grep for. Notice bash in that list too: the shell you're typing
into is just another process, no more special than the rest.
ps output is just text — which means Text & Pipes and Text Surgery already taught you how to search it. ps aux | grep node finds a process by name, and awk '{print $2}' pulls the PID column out of the result. This is why the column-shaped
output of old Unix tools is worth putting up with: every tool composes. There's a shortcut for
the common case too — pgrep node prints matching PIDs directly.ps is a photograph. For the movie, top repaints the same table
live, a few times a second, worst offenders first — and q gets you out, the same
escape as every pager (Looking Inside Files). Its nicer cousin is htop, the very tool sudo's install example
happened to pick. Between them: ps answers "what exactly is running?", top answers "what is my machine doing right now?""What's using all my CPU right now? Show me how to check, and explain the columns"
"Find the process id of my running dev server without me having to read the whole ps output"
8.2 Stopping Things — Ask Nicely, Then Insist
kill has a violent name and a polite default. Plain kill PID sends SIGTERM —
a signal, which is a short fixed message
the operating system delivers to a running process from outside, nothing to do with whatever
the program normally reads. They come in a small vocabulary and all share a prefix: SIG for signal, then what it asks for, so SIGTERM is "terminate"
— please finish up and stop. The program gets to react: save its work, close its files, remove
its lock, and exit cleanly. It's a letter, not a bullet.
kill 437 # SIGTERM, signal 15 — "please stop when you can"
(no response — spinner.sh --forever is still running)
kill -9 437 # SIGKILL, signal 9 — the floor opens, no cleanup
[killed] spinner.sh --forever (PID 437)The 9 is a signal number, not a count: every signal has one, kill -N means "send signal N", and a bare kill is kill -15. That's how you read kill -HUP or kill -2 in an agent's command — same verb, different message.
A program can catch SIGTERM and decide to ignore it — that's a feature, not a bug: it's how servers
finish serving the request they're mid-way through instead of dropping it. But it also means a
polite kill can bounce. kill -9 sends SIGKILL, which no program can catch,
refuse, or prepare for. The kernel — the
core of the operating system, the part that owns the hardware and creates every process,
including your shell — simply removes it. Nothing gets saved and no cleanup runs, so any lock file it was holding stays behind: the
marker a program leaves to say "I'm already running". The next copy finds it, believes it, and
refuses to start.
kill -9 is a last resort, not a default. Plenty of guides
(and plenty of AI answers) reach for it first because it always works. It always works the
way an axe always opens a door. Ask nicely, give it a second, and escalate only if it
refuses — and treat kill -9 as a red flag when an agent reaches for it without
trying the polite version first — one more entry for the audit routine in Terminal for the AI Era.One thing you already knew, renamed: Ctrl+C is a signal too — SIGINT, "interrupt" — sent to whatever is
running in the foreground — the process
attached to this terminal right now, the one your keystrokes reach and the reason your
prompt hasn't come back. That's why it stops a stuck command but does nothing to a server
running in another window; that one needs its PID. How the Terminal Works opens up the
machinery underneath all three signals.
"A process is ignoring kill — walk me through what to try, in order, and what each step costs"
"What actually happens to unsaved work when I use kill -9 versus a plain kill?"
Try It: Stop the Runaway
ps aux,
try kill first and read what happens — then escalate. Leave the innocent server
running.Loading playground...
8.3 Who's on Port 3000?
A port is a numbered door on your
machine. A server opens one and waits there for visitors; your browser knocks on it when you
visit localhost:3000 — localhost being your machine's name for
itself (What Is localhost?). The numbers themselves are convention rather than
law: 3000 and 5173 are what particular tools happen to pick, and everything here works the
same for 8080.
The rule that generates all the pain is simple: one program per port. Try to open a door
someone's already standing in — here with serve, a tiny dev server you
install with npm, playing the part of whatever starts your project — and you
meet the error:
serve
Error: listen EADDRINUSE: address already in use :::3000
lsof -i :3000 # who's holding the door?
COMMAND PID USER TYPE NODE NAME
node 400 vibe IPv4 TCP *:3000 (LISTEN)
kill 400 # ask yesterday's server to leave
serve # the pier is free
serve: listening on http://localhost:3000That's the same three beats from the start of this part, with the error that sent you
looking at the front and your own server at the end: read the error, find the PID, stop the
squatter, start yours. Memorize it and one of the most common blockers in web development
becomes routine. lsof stands for "list open files"; the -i flag narrows it to network
connections. Silence from lsof means the port is free.
That one output line is worth reading properly. node is the program and 400 is its PID — the only field you actually need. The rest describes the connection
it's holding, and a real lsof prints a few more of those columns than you see
here. IPv4 TCP says what kind of connection it is; *:3000 is the door number with a wildcard host, where * means
"any address on this machine" rather than the filename glob from Wildcards — and the :::3000 in the error above is that same wildcard, spelled the IPv6
way. (LISTEN) is the confirmation: that process is sitting at the door waiting for
visitors, which is exactly why yours can't have it.
lsof -i :3000 tells you its name."My dev server says EADDRINUSE on port 5173 — give me the exact commands to find and stop what is holding it"
"How do I start my app on a different port instead of killing what is already there?"
Try It: Free Port 3000
serve to meet the error, then work the ritual: lsof -i :3000 for the PID, kill it, and start your own.Loading playground...
8.4 Background & Foreground
A dev server holds your terminal hostage: it runs until you stop it, and the prompt never comes back. One answer is more terminals (the tabs and splits in Many Terminals at Once). The other is job control — telling this shell to keep the program running backstage while you get your prompt back.
./slowbuild.sh & # & = start it, give me my prompt back
[1] 400 # job number, then PID
jobs # who's backstage?
[1] Running ./slowbuild.sh
fg %1 # bring job 1 into the spotlightTwo numbers appear here and they're easy to confuse: the job number in [1] is small
and belongs to this shell — you use it as %1. The PID is large and belongs to the whole
machine. kill %1 and kill 400 stop the same program here.
A background job is still yours and still
tied to this shell: close the window and it goes with it, unless you take extra steps. The
step has a name — nohup, "no hangup," from the days when closing a terminal
literally hung up a phone line: nohup ./slowbuild.sh & keeps running after the
window is gone and parks its output in nohup.out. It works, but it's a
patch; the comfortable answer to work that must outlive your window is a terminal that
survives on its own, which is tmux in Many Terminals at Once.
Either way, backstage is not the same as somewhere safe — don't send a long build back there
bare and then walk away from the terminal.
There's one more move, and it's the rescue you'll actually reach for: you started something
long, forgot the &, and now you're stuck watching it. Ctrl+Z suspends it and hands your prompt back — suspended meaning
stopped dead, making no progress at all, which is why jobs reports it as Stopped rather than Running. bg is what resumes
it, backstage: the place you'd have been with &, arrived at the hard way.
Four ampersands, no relation. 2>&1 is a reference to a stream (Redirection). && runs the next thing only if this one worked (Exit Codes & Chaining). & inside a sed replacement means whatever was just matched
(Find & Replace). And a bare & at the end of a line is the one
this section is about — reading npm run build & as a half-typed && is a mistake worth not making.
%1 and %2."I started a long command and forgot the & — how do I get my prompt back without losing the work?"
"What's the difference between a job number and a PID, and when does each one matter?"
Try It: Two Things at Once
&, confirm it's there with jobs, then bring it forward with fg %1 and check what it left
behind.Loading playground...
You can now see what's running, stop it politely or firmly, and free a port that's been taken hostage — the three moves that unstick a machine. Next: the server you just started is waiting on port 3000. Time to talk to it.
Loading challenge...