If your Node.js API freezes, it’s usually not “Node being slow.” It’s heavy OS work + broken async control.

Two classic mistakes:

  • Using exec() for big tasks

  • Letting promises and processes run forever

Problem #1: exec() Eats Memory

exec() buffers all output. Large PDFs, videos, or logs will:

  • Blow up RAM

  • Block the event loop

  • Freeze your API

Problem #2: Promises That Never Stop ☠️

Even with spawn, you can leak CPU if you:

  • Use Promise.race() without killing the loser

  • Resolve/reject a promise but leave the process running

  • Forget timeouts or signal handling

Result: zombie processes burning CPU.

The Right Way: spawn + Lifecycle Control

Example using a simple CLI command (ffmpeg), but this applies to any tool:

const { spawn } = require('child_process');

function convertVideo(input, output, timeout = 15000) {
  return new Promise((resolve, reject) => {
    const ffmpeg = spawn('ffmpeg', ['-i', input, output]);

    const timer = setTimeout(() => {
      ffmpeg.kill('SIGKILL'); // kill the real work
      reject(new Error('Conversion timed out'));
    }, timeout);

    ffmpeg.on('close', code => {
      clearTimeout(timer);

      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`ffmpeg failed (${code})`));
      }
    });

    ffmpeg.on('error', reject);
  });
}

This guarantees:

  • No buffered output

  • No dangling promises

  • No runaway CPU

spawn vs fork

  • Use spawn: CLI tools (ffmpeg, git, docker, gs, python)

  • Use fork: Only for another Node.js process with IPC

Takeaway

Freezing Node apps aren’t just a memory problem. They’re a control problem.

  • Stream heavy work with spawn

  • Always resolve or reject

  • Kill what you start

Don’t forget that!