DEV Community

nareshipme
nareshipme

Posted on

Why ffmpeg astats Crashes Node.js child_process (And the One-Line Fix)

If you're using ffmpeg from Node.js via execFile or execSync, there's a subtle way it can crash your app: stdout/stderr buffer overflow.

I ran into this while building an audio analysis pipeline that scores video segments by energy, silence, and dynamic range. Everything worked in development on short clips. In production, on a 45-minute video? Instant crash.

The Setup

We needed RMS volume and peak levels for audio segments. The natural ffmpeg approach is astats:

import { execFile } from "child_process";
import { promisify } from "util";

const execFileAsync = promisify(execFile);

async function analyzeRms(audioPath: string, start: number, duration: number) {
  const { stderr, stdout } = await execFileAsync(
    "ffmpeg",
    [
      "-ss", String(start),
      "-t", String(duration),
      "-i", audioPath,
      "-af", "astats=metadata=1:reset=1,ametadata=print:file=-",
      "-f", "null",
      "-",
    ],
    { timeout: 30000 }
  );

  const output = stderr + stdout;
  const rmsMatch = output.match(
    /lavfi\.astats\.Overall\.RMS_level=(-?\d+(?:\.\d+)?)/
  );
  // ... parse and return
}
Enter fullscreen mode Exit fullscreen mode

This looks fine. It is fine — for short audio.

The Problem

astats with metadata=1:reset=1 combined with ametadata=print:file=- prints a line of metadata for every single audio frame. On a 45-minute file at 44.1kHz, that's millions of lines dumped to stdout.

Node.js execFile buffers all of stdout and stderr in memory. The default maxBuffer is 1MB. When the output exceeds that:

Error: stdout maxBuffer length exceeded
Enter fullscreen mode Exit fullscreen mode

Your process throws, your pipeline dies, and you're staring at an error that says nothing about ffmpeg or audio.

The Naive Fix (Don't Do This)

You might think: just bump maxBuffer.

{ timeout: 30000, maxBuffer: 50 * 1024 * 1024 } // 50MB
Enter fullscreen mode Exit fullscreen mode

This "works" but now you're buffering 50MB of text in memory per ffmpeg call. If you're analyzing multiple segments concurrently (we analyze all topic segments in parallel), that's hundreds of megabytes of throwaway strings. On a memory-constrained container (ours runs at 460MB heap), that's a death sentence.

The Real Fix: Use volumedetect

ffmpeg has a filter called volumedetect that does exactly what astats does for volume analysis — but it outputs a compact summary instead of per-frame data:

const { stderr } = await execFileAsync(
  "ffmpeg",
  [
    "-ss", String(start),
    "-t", String(duration),
    "-i", audioPath,
    "-af", "volumedetect",
    "-f", "null",
    "-",
  ],
  { timeout: 30000, maxBuffer: 5 * 1024 * 1024 }
);

const meanMatch = stderr.match(/mean_volume:\s*(-?\d+(?:\.\d+)?)\s*dB/);
const maxMatch = stderr.match(/max_volume:\s*(-?\d+(?:\.\d+)?)\s*dB/);
Enter fullscreen mode Exit fullscreen mode

The output looks like this — always just a few lines, regardless of input length:

[Parsed_volumedetect_0 @ 0x...] n_samples: 2116800
[Parsed_volumedetect_0 @ 0x...] mean_volume: -28.2 dB
[Parsed_volumedetect_0 @ 0x...] max_volume: -3.1 dB
[Parsed_volumedetect_0 @ 0x...] histogram_3db: 42
Enter fullscreen mode Exit fullscreen mode

Four lines. Not four million.

Mapping volumedetect to Scores

mean_volume gives you average loudness (replaces RMS_level). max_volume gives you peak (replaces RMS_peak). The math to normalize them into 0-100 scores:

// mean_volume typically ranges from -50dB (very quiet) to -15dB (loud)
const energy = clamp(((rmsDb + 50) / 35) * 100, 0, 100);

// Dynamic range = difference between peak and mean
if (maxMatch) {
  const maxDb = parseFloat(maxMatch[1]);
  const dynamicRange = maxDb - rmsDb;
  dynamism = clamp((dynamicRange / 20) * 100, 0, 100);
}
Enter fullscreen mode Exit fullscreen mode

The General Lesson

When using ffmpeg (or any CLI tool) from Node.js:

  1. Know your output size. Per-frame filters (astats, showinfo, ametadata print) scale with input length. Summary filters (volumedetect, silencedetect) don't.

  2. Don't just bump maxBuffer. It hides the real problem and shifts the failure from a clear error to a mysterious OOM.

  3. Always set maxBuffer explicitly anyway. The 1MB default is too low for most ffmpeg stderr output (which includes progress lines). We use 5MB as a safety net:

{ timeout: 30000, maxBuffer: 5 * 1024 * 1024 }
Enter fullscreen mode Exit fullscreen mode
  1. Consider spawn for truly large output. If you need per-frame data, use spawn and stream the output instead of buffering it all. execFile is for commands with bounded output.

TL;DR

Filter Output Safe for execFile?
astats + ametadata print Per-frame (millions of lines) ❌ Overflows
volumedetect Summary (4 lines) �u2705 Always safe
silencedetect Per-silence-region �u2705 Usually safe
showinfo Per-frame ❌ Overflows

Switch to volumedetect. Your containers will thank you.

Top comments (0)