Repository navigation
Description
Describe the bug
On Windows, spawning the managed ffmpeg/ffprobe binaries can fail transiently with EBUSY/ETXTBSY when the executable file is briefly locked — overwhelmingly antivirus real-time scanning a just-written or just-executed exe, occasionally a lingering handle from a concurrent sibling invocation (a single render shares one managed ffmpeg binary across many stages: media probing, frame extraction, audio mix, encode).
When that happens, the whole render fails with a bare, context-free error and there is no retry:
Error: spawn EBUSY
We observed this repeatedly in an Electron integration of the producer: a Delivery transcode step used the same managed ffmpeg.exe seconds before the render pipeline started; the render then failed on its first ffmpeg/ffprobe spawn and the user-facing export was lost. Re-running the export afterwards succeeded — confirming the condition is transient.
Root cause
All three spawn sites surface the raw child-process error without classification or retry:
packages/engine/src/utils/runFfmpeg.ts— the spawn error arrives inside theManagedChildProcessoutcome (reason: "spawn_error", rawerror).packages/producer/src/services/audioExtractor.ts(runFFmpeg) —ffmpeg.on("error", reject).packages/producer/src/services/render/audioPadTrim.ts(runFfprobeJson) — throwsoutcome.errorraw.
Node reports these as Error: spawn EBUSY (with .code set), so neither the user nor the logs can tell which binary, which stage, or that the condition usually clears on retry.
Expected behavior
- A transient file-lock spawn failure should be retried a few times with a short backoff — a fresh spawn almost always succeeds once the AV scan finishes.
- When retries are exhausted, the error should name the binary and the errno, and hint at the usual cause.
Environment
- Windows 11 x64 (applicable to any Windows host with real-time AV scanning)
- Producer used via an embedded runtime, ~v0.8.40
ffmpeg.exe/ffprobe.exeresolved via managed binary paths
Proposed fix
A shared engine helper, withTransientSpawnRetry, that:
- classifies
EBUSY,ETXTBSY,EAGAIN,EMFILE,ENFILEas transient file-lock errnos; - re-runs the caller's existing spawn-and-wait flow with exponential backoff (250 ms base, up to 3 attempts) and logs each retry with the binary path;
- enriches exhausted failures with the binary path, errno, and a "transient file lock (often antivirus real-time scanning)" hint;
- wraps the caller's flow rather than the raw
spawn()call, so a healthy spawn keeps its exact synchronous timing.
A PR is on the way.
Activity
chiarorosa commentedon Oct 16, 2026
I'd like to take this one. The review on the closed #4059 asked for the real errno from Windows hardware, so I measured that first. It changes the shape of the fix.
What Windows actually returns
Windows 11 Pro 10.0.26200 x64, Node v24.20.0, ffmpeg 9.0.2 (gyan.dev). I spawned a copy of
ffmpeg.exewhile a second process held it open with different .NETFileAccess/FileSharemodes. Defender real-time protection was off, so this simulates the lock instead of catching an AV scan in the act:spawn()resultFileShare.NoneEBUSY(-4082), syscallspawnFileShare.ReadEBUSYFileShare.ReadWriteEBUSYFileShare.ReadEPERM(-4048)'error'eventENOENTRewriting the exe and spawning it right away 30 times, and spawning it 64 times at once without a lock, never failed. So the "sibling invocation" theory doesn't reproduce, but any handle with write access or no sharing does.
Two consequences:
EBUSYis the right errno, andEPERMshould stay out: hereEPERMis the permanent ACL case.EBUSYis thrown synchronously fromspawn(). Node only deliversEACCES,EAGAIN,EMFILE,ENFILEandENOENTthrough the'error'event. Everything else throws before aChildProcessexists (ChildProcess.prototype.spawn). SoManagedChildProcess.onError/reason: "spawn_error"never sees it. A retry keyed on thespawn_erroroutcome (the approach in fix(engine,producer): retry transient Windows spawn file locks on ffmpeg/ffprobe #4059) would not fire for this bug onrunFfmpeg,ffprobe.tsoraudioPadTrim.What each managed-binary call site does today
There are 10 call sites that start
getFfmpegBinary()/getFfprobeBinary()outside tests. With a syncEBUSY:spawn EBUSY:runFfmpeg.ts(spawnFfmpeg),ffprobe.ts(runFfprobe),streamingEncoder.ts,audioExtractor.ts(inside thenew Promiseexecutor),audioPadTrim.ts(runFfprobeJson),gpuEncoder.ts(detectGpuEncoder),psnr.ts,distributed/shared.ts(readFfmpegVersion).gpuEncoder.tscanUseGpuEncoder→.catch(() => null)caches "no usable GPU encoder", andpsnrFilterAvailability.tscachesnull("no filters") after its catch-all.Proposed fix
spawn()throw with codeEBUSYorETXTBSY(the POSIX equivalent). Nothing has started at that point, so the retry has no side effects. I'm proposing 3 attempts with 250 ms / 500 ms backoff, each retry logged with the binary path. TheexecFile-based sites (psnr,psnrFilterAvailability,readFfmpegVersion) retry the same rejection..codeand names the binary path and errno, with the hint "another process held the executable open (often antivirus scanning it)".spawnthat throwsEBUSYn times, plusEPERM/ENOENTpassing straight through), and a per-site test that the retry is wired in. I'll also attach the Windows repro script to the PR.Open question for @miguel-heygen / @miga-heygen: is 3 attempts / ~750 ms total acceptable, or would you rather allow a longer window (for example 5 attempts, ~4 s) since a full AV scan of a 100+ MB
ffmpeg.execan take longer?fix(engine): retry a spawn that fails because ffmpeg/ffprobe is locked
fix(producer): retry ffmpeg/ffprobe spawns that hit a locked executable
fix(engine): retry a spawn that fails because ffmpeg/ffprobe is locked
fix(producer): retry ffmpeg/ffprobe spawns that hit a locked executable
miguel-heygen commentedon Oct 24, 2026
Thanks @CasbaL for the report, and @chiarorosa for measuring this on real Windows hardware. Showing that
EBUSYis thrown synchronously fromspawn(), and so never reaches thespawn_errorpath, explains why the first attempt (#4059) would not have helped.I checked current main (0.8.193): no spawn retry yet.
runFfmpegstill callsspawn()directly, so a locked executable still fails the render with a barespawn EBUSY.The fix is in #5698, which has review feedback to address. Discussion of the retry window is best kept on that PR so it stays in one place.
fix(engine): retry a spawn that fails because ffmpeg/ffprobe is locked
fix(producer): retry ffmpeg/ffprobe spawns that hit a locked executable
fix(engine): retry a spawn that fails because ffmpeg/ffprobe is locked
fix(producer): retry ffmpeg/ffprobe spawns that hit a locked executable