TerminalVibes / Processes & Ports
Part 8

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.

Important
This is the part that turns "I'll just restart my laptop" into a ten-second fix. Every technique here is the same three-beat move: find the process, get its number, act on that number.

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:

Every running program is a row with a number
Reading the census
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 --forever

The 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.

Tip
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.
Note
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?"
Vibe it

"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.

SIGTERM asks. SIGKILL removes the floor. Ask first.
The escalation, in order
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.

Caution
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.

Vibe it

"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

Your fans are screaming. Find the process burning 97% of a core with ps aux, try kill first and read what happens — then escalate. Leave the innocent server running.
Stop the Runaway

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:3000localhost 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:

One ship per pier — that's the whole rule
The error, and the fix
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:3000

That'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.

Tip
Where do stale servers come from? Usually an agent that started one in a background shell — a whole separate session you never saw, which is not the same thing as the background jobs in Background & Foreground below. When a port is mysteriously busy right after an AI session, that's the first suspect — and lsof -i :3000 tells you its name.
Vibe it

"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

Yesterday's server never died. Run serve to meet the error, then work the ritual: lsof -i :3000 for the PID, kill it, and start your own.
Free Port 3000

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.

& sends it backstage · jobs lists it · fg brings it into the spotlight
Backstage and 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 spotlight

Two 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.

Tip
Honest advice: use tabs. Job control is a genuinely useful escape hatch — and mostly a rescue for the terminal you're already in. For an everyday setup (server in one pane, logs in another, a free shell for you), the split terminals in Many Terminals at Once are nicer to live with than juggling %1 and %2.
Vibe it

"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

Send the slow build backstage with &, confirm it's there with jobs, then bring it forward with fg %1 and check what it left behind.
Two Things at Once

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.

Challenge: Clear the Agent's Processes

Loading challenge...