---
title: "A Load Average of 4.2 and Six `next dev` Processes Nobody Started"
canonical: https://dxdev.com/blog/orphaned-resources-automatic-cleanup/
datePublished: 2026-07-23
---
The load average sat at 4.2 on a box that should idle at 0.3. `ps aux` showed six `next dev` processes, the oldest three days alive, none of them attached to a terminal anyone was looking at. Somewhere in that list was a `find /` that had been walking the filesystem for eleven hours, kicked off by a script that grepped for a config file and never scoped the search.

We went looking for why everything felt slow. There was no crash and no alert. The machine was just losing capacity to processes nobody remembered starting.

## Why the servers piled up

The dev-server launcher had no memory of what it had already started. Every session that needed a preview ran `npm run dev` (or the equivalent for whichever repo it was in) and bound to whatever port was free. If port 3010 was taken, it grabbed 3011. Next session, another port.

Sessions end all the time in this setup. Some end cleanly, and some end because the agent got killed or the terminal closed without a shutdown signal. The server keeps running either way. Nothing in the lifecycle ever asked "is this already up" or "should this still be up."

Six months of that gives you six orphaned Next.js dev servers. Each holds a chunk of memory and does hot-reload file watching on a repo nobody is editing anymore. Add a background `find` that an earlier session left crawling the disk for a lockfile, when it should have searched a targeted path.

## The wrong turn: killing it by hand

Our first move was the obvious one. We wrote a one-off script to `pkill -f "next dev"` and clear the `find`. It worked, and the load average dropped back to normal in about ten seconds. We closed the ticket in our heads and moved on.

It came back within a day. We hadn't touched the thing creating the orphans, so we had swept the floor once. The launcher still had no idea a previous instance existed, so the next session spun up its own dev server on the next free port. The process that had been orphaned an hour earlier was still sitting there, because nothing had told it to die.

Manual cleanup treats the symptom each time you remember to run it and does nothing the rest of the time. The cost was a day of believing the problem was solved while the leak kept compounding under a load average that looked fine right after we checked it.

## What actually fixed it

The fix had two pieces, tracked under a single internal ticket.

**An idempotent dev-server launcher.** Before starting anything, it checks for an existing process bound to the port or matching the repo path. It uses a PID file for this, because grepping process names is fragile. If one is already running and healthy, the launcher reuses it and returns immediately instead of spawning a duplicate. If the PID file points at a dead process, it cleans the stale record and starts fresh. The same command run five times in a row produces one server, not five.

**The reaper.** This is a script that walks known process patterns: dev servers, long-running `find`/`grep` invocations past a runtime threshold, and orphaned bridge spawns. It kills anything that no longer has a live session behind it.

The key design choice was where it runs. It is wired directly into session start and session close, and it is not a cron job hoping to catch things eventually. On start, it sweeps anything orphaned by the previous session before doing any work. On close, it sweeps anything the session itself spun up that is still alive. That covers both ends. Garbage can't accumulate between sessions, and a session can't leave garbage behind when it ends, clean or not.

Together they mean the launcher won't create a duplicate, and the reaper cleans up the ones that existed before this shipped. It also catches anything that slips through some other way, such as a hard kill, a crashed terminal, or an orphaned bridge spawn from the handoff path.

We committed and pushed it the same session, then verified against the running box. The load average was back to 0.3, and `ps aux` showed exactly the processes that had live sessions behind them, nothing else.

## Why session boundaries instead of a timer

We could have made the reaper a cron job running every fifteen minutes. A timer-based sweep still lets orphans live for however long the interval is, and it runs whether or not anything needs sweeping. Hooking the reaper to session start and close means the check happens exactly when the conditions that create orphans are occurring, which is a session ending or a new one beginning. That is fewer checks, and each one is meaningful.

If your development machines slow down over time, look at two things. Does the launcher track what it has already started, and does anything clean up when a session ends? For us, the launcher plus a reaper wired to session start and close took the load average from 4.2 back to 0.3.
