DEV Community

Cover image for macOS runs out of application memory because your dead dev servers never die
Mirza Iqbal
Mirza Iqbal

Posted on

macOS runs out of application memory because your dead dev servers never die

It was past midnight and my Mac threw a box I had not seen in years.

Your system has run out of application memory.

Force Quit Applications.

I had four things open. A browser, an editor, a terminal, and a music app that uses about nothing. None of that should fill 36 gigabytes of RAM and then push into swap until the kernel begs you to start closing things.

You have seen this popup. You closed the browser, you closed the editor, you felt a little betrayed by your own machine, and you got maybe twenty minutes before it came back.

Here is the part nobody tells you.

Your memory was not being eaten by the apps you could see. It was being eaten by the ones you could not.

Dev servers you thought you closed are still running

Every time you run a local dev server, you spawn a process. next dev, vite, webpack, an esbuild watcher, a tsc watcher, nodemon, or a plain pnpm start. It listens on a port, it watches your files, it holds memory.

When you press Ctrl plus C, it dies. Most of the time.

But close the terminal tab instead of pressing Ctrl plus C, and the signal that would have killed it never arrives cleanly. A wrapper you launched dies and the real worker underneath keeps running. On my machine the actual server runs as a child process, and killing the parent left the child alive, still bound to the port, still holding memory.

Do that across a week of work. Open a tab, start a server, close the tab, move to the next project. Each one leaves a quiet survivor behind.

By the time the popup fired, I had three production style servers still running from sessions I had ended days earlier. Each one was small on its own. Together with the file watchers, they were the whole problem.

Here is what makes local infrastructure rot nasty. It never shows up in a demo. It shows up at midnight on your own machine, and it looks like your Mac is broken when your Mac is fine.

Why closing the terminal is not the same as stopping the server

If you want one line to remember, here it is.

A dead terminal does not mean a dead server.

Three common ways this happens.

  • You close the terminal tab or window instead of stopping the process. A GUI close does not deliver the interrupt the same way Ctrl plus C does, so the server is orphaned rather than killed.
  • You start a server through a wrapper like npm start, then Ctrl plus C kills the wrapper shell but leaves the node process it spawned running in the background.
  • A tool launches the server as a child of a child. Signalling the top process never reaches the leaf, and the leaf is the one holding the port.

Then you pay the daily tax. You start the server again and the terminal shouts EADDRINUSE, address already in use. A leftover you forgot about is still sitting on port 3000, and now you cannot even start the thing you actually want.

So you do what every answer on the internet tells you to do.

You run killall node.

Why kill all node is the worst possible fix

killall node feels great for about one second.

Then you notice your editor's language server is dead. Your other project's dev server is dead. Your database GUI is dead. And if you run AI tooling with local model context servers, those are gone too, because they are node processes, and killall node does not care which node you meant.

killall node does not target one thing. It matches by name and stops every match on the entire machine. So does pkill dash f node. So does the classic ps piped into grep piped into xargs kill. They are all the same weapon pointed at your own foot.

Here is my strongest opinion in this whole piece.

You should never stop a process by name. Not node, not next, not anything. You stop the exact process by its process id, so nothing else can get caught in the blast.

People reach for the name based version because finding the one right process id is annoying, and the popup makes you panic. Panic plus a blunt instrument is how you turn a small leftover into a lost afternoon.

What a safe cleanup actually has to get right

I lost that afternoon. So I stopped hand hunting process ids and built a small system that finds the genuine leftovers and stops only those, and I put it through a hard review before I trusted it to run on its own.

I am not going to hand you the whole recipe, because the value is in the safety reasoning, not the shell. But here is everything you need to think clearly about it, and to know why the naive version is dangerous.

A safe reaper has to answer five questions before it stops anything.

  • Is this a leftover, or a server I am using right now. An orphaned process on macOS reparents to the system launcher, which also legitimately owns real daemons. A parentless process on its own is not proof of anything. You have to combine it with a dev server signature before you trust it.
  • Is it actually a server, or a one shot build. Stopping a running build halfway corrupts the artifact. A real server listens on a port. A build does not. That difference is your safest gate.
  • Is anyone using it. A dev server with a browser tab open has live connections. A truly abandoned one has none. Stop the idle one, spare the connected one.
  • Is it something the user set up on purpose. A background service someone installed as a launch agent looks identical to an orphan, and stopping it starts a fight where the launcher keeps respawning it and your cleanup keeps stopping it.
  • Am I about to hit my own tools. Model context servers, editor language servers, browsers, and the agent itself all have to be excluded by design, not by luck.

Only when all of those line up is a process a genuine, safe target.

One more trap, measuring the wrong number

Here is a subtle one that cost me real time.

That out of application memory popup fires on swap and kernel memory pressure, never on how much RAM looks free.

I measured it live during the incident. My machine reported a comfortable percentage of memory free while swap was ninety six percent full with barely a gigabyte left. Trigger your cleanup on free memory percentage and it will sleep through the exact moment the popup is about to fire, because the number it reads looks fine.

What matters is the kernel's own memory pressure level and how much swap is actually left. Watch those, not the friendly percentage.

What made it reliable

A single script version was not enough, because one point of enforcement is one point of failure. My version has layers that each cover the others.

  • A detection and cleanup core that classifies leftovers and stops them gracefully, by process id, oldest and idlest first.
  • A check that runs when a new session starts, surfaces the pileup, and cleans the safe ones.
  • A background timer so it also runs when I am not looking, because pileup happens while you sleep.
  • A guard that blocks me from ever typing the name based mass kill in the first place and points me at the safe path instead.
  • Tests that spawn fake servers and prove the thing stops the leftover and never touches the process next to it.

Graceful shutdown matters. You send the polite signal first and wait. You only escalate to the hard stop for the stragglers that ignore you. You never lead with the hard stop, because that skips cleanup, can corrupt the terminal, and orphans the very children you were trying to catch.

Now the honest part

I got the first version wrong.

I had it group stopping more aggressively than it should, and in one early test it very nearly signalled my own shell. I had it measuring the wrong memory number. I had a guard that could be sidestepped by a command that merely mentioned the safe tool by name.

I only caught those because I refused to trust my own code and ran it past a hard, adversarial review from a systems angle and a safety angle before it went live. Its verdict on my first draft was blunt. Not safe to auto run as is. Every one of those holes is closed now, and the fact that they existed is the whole point. This class of tool is easy to write and easy to write dangerously.

That is the real lesson, bigger than the popup.

Scary infrastructure is the kind that runs on your behalf and can quietly do the wrong thing. A cleanup that stops your editor is worse than the mess it cleaned. So you build it to be paranoid, you make it prove itself against the exact things it must never touch, and you never let it act on a signal it only half understands.

What to check tonight

Before you close your machine.

  • Look for dev servers you thought you already stopped. Odds are you have a few.
  • When you find a stuck one, stop it by its process id, not by its name.
  • Never reach for killall node. It is the fix that creates three new problems.
  • If you build any automation that stops processes, make it target one id, exclude your own tools by design, and act on real memory pressure, not a comfortable percentage.

I run the hardened, reviewed version on my own machine now, and I help teams clean up exactly this kind of invisible infrastructure rot, the stuff that never breaks a demo and quietly eats a Tuesday. If that is a fire you are fighting, the ideas above are enough to start, and safe execution is where it gets interesting.

Your turn

What is still running on your machine right now that you thought you closed hours ago.

If this was useful

I work through this in public, the wins and the messy afternoons both, mostly on LinkedIn and YouTube. If the real version of building in the open is useful to you, that is where it lives. Find me on X, GitHub, and the work at next8n.com.

Top comments (0)