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
}
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
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
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/);
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
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);
}
The General Lesson
When using ffmpeg (or any CLI tool) from Node.js:
Know your output size. Per-frame filters (
astats,showinfo,ametadata print) scale with input length. Summary filters (volumedetect,silencedetect) don't.Don't just bump
maxBuffer. It hides the real problem and shifts the failure from a clear error to a mysterious OOM.Always set
maxBufferexplicitly 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 }
-
Consider
spawnfor truly large output. If you need per-frame data, usespawnand stream the output instead of buffering it all.execFileis 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)