Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/17/20264 min read

Your Next.js build hangs at 0% CPU: the broken pipe nobody logs

Your Next.js build hangs at 0% CPU: the broken pipe nobody logs

A production build that has been running for half an hour. ps says the process is alive. top says it is using almost no CPU. The log has not moved. Nothing has failed, and nothing is happening.

The usual suspects — memory pressure, a slow type-check, a huge page count — all produce busy processes. This one is idle. That difference is the whole diagnosis.

What actually happened

next build writes progress to stdout, inherited from whatever started it. Start a build over SSH and stdout is the SSH session's pipe.

Drop that session — connection lost, terminal closed, parent process killed — and the read end of the pipe goes away. The build keeps running until its next write to stdout. That write has nowhere to go, so it blocks. The process is now waiting, forever, on a write nobody will read.

It will not time out. It will not error. It has no idea anything is wrong; from its point of view it is mid-console.log.

The signature is unmistakable once you know it:

  • process present in ps
  • CPU at or near 0%
  • output file not modified for as long as the hang has lasted

Why it is worse than a stuck build

Here is the part that turns an annoyance into an outage.

next build clears the output directory when it starts. A hung build has therefore already deleted the previous .next contents and has not finished writing the new ones. There is no BUILD_ID and no static directory on disk.

Meanwhile the site is still up, because the running server loaded what it needed at startup and is serving from memory. Everything looks fine.

Restart that server — to "fix" the stuck build, or because a process manager restarted it for unrelated reasons — and it comes up against a half-empty output directory and fails. The hang was survivable; the restart is the outage.

So the first thing to establish is not how to un-stick the build. It is: do not restart the app until a complete build exists. Check for the build id before anything else:

cat .next/BUILD_ID || echo "INCOMPLETE — do not restart"

Running builds so it cannot happen

Detach the build from the session's stdout entirely:

setsid nohup npm run build > /tmp/build.log 2>&1 < /dev/null &

Every piece of that earns its place. setsid puts the build in a new session so it is not signalled when the terminal goes. nohup covers SIGHUP. Redirecting stdout and stderr to a file means writes always have a reader. < /dev/null stops anything blocking on input.

Then poll the artefact rather than watching the log:

until [ -f .next/BUILD_ID ] || ! pgrep -f "[n]ext build" >/dev/null; do
  sleep 10
done

That exits on success or on the build dying, which is what you want — a loop that only waits for success hangs as thoroughly as the build did.

The cleanup mistake worth avoiding

The obvious way to clear a stuck build is also a trap:

pkill -f "next build"      # matches its own command line

pkill -f matches against full command lines — including the command line of the shell you just launched to run pkill, which contains the string next build. Over SSH this frequently kills your own session before anything else happens, and the commands you chained after it never run.

You then look at the log file, see a complete successful build, and conclude the rebuild worked. It was the previous build's log, hours or days old. Check the modification time before you trust a log you did not just watch:

ls -la --time-style=full-iso /tmp/build.log

The bracket trick avoids the self-match, because [n]ext does not literally appear in the pattern string as written:

pgrep -f "[n]ext build"

The checklist

1. CPU near zero and no file writes? Suspect a blocked write, not slow work.

2. Check .next/BUILD_ID before restarting anything.

3. Kill the hung build, then rebuild detached.

4. Wait on the artefact, not the log.

5. Restart the app only once the build id exists.

The underlying lesson generalises past Next.js: any long-running process that inherits a terminal's stdout is one dropped connection away from hanging on a write. Builds, migrations, seed scripts, backups. If it runs for more than a minute on a remote host, detach it.

Related articles