Skip to content

Build1 publisher3 min readPublished

FFmpeg.wasm ships, but budget for a watchdog: one in ten heavy jobs deadlocked in silence

A developer running media conversion entirely in the browser reports that FFmpeg.wasm hung with no error and no log on roughly one heavy job in ten until he added a 180 second idle timer.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Photograph accompanying FFmpeg.wasm ships, but budget for a watchdog: one in ten heavy jobs deadlocked in silence
Photo: dev.to

What happened

  • FFmpeg.wasm can hang forever with no error, no log and no way out except killing the browser tab.
  • The silent hang happened on roughly one in ten heavy jobs before the author shipped a watchdog.
  • The author calls the deadlock only the third-worst problem he hit while building a media converter that runs entirely in the browser.
  • In the multithreaded build @ffmpeg/core-mt, Emscripten creates a limited pool of pthreads roughly matching navigator.hardwareConcurrency; if the decoder and encoder request more threads at the same time than the pool holds, pthread_create blocks the runtime forever, with no errors and no logs.
  • The user-visible symptom of the deadlock is a frozen progress bar, with no other external signs.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A developer building a client-side media converter has published a post-mortem saying that FFmpeg.wasm, in its multithreaded build, can hang indefinitely with no error, no log output and no recovery path except killing the browser tab [1]. By his account this happened on roughly one in ten heavy jobs before he shipped a watchdog [2], which is a failure rate around 10 percent on exactly the workloads users care about [9].

The mechanism is specific and worth knowing before you commit. With `@ffmpeg/core-mt`, Emscripten creates a pthread pool roughly matching `navigator.hardwareConcurrency`; if the decoder and encoder together ask for more threads than the pool holds, `pthread_create` blocks the runtime permanently [4]. There is no exception to catch and nothing in the log. The user sees a progress bar that has stopped moving [5].

The fix in the post is not a fix, it is a detector. Every log line and every progress event refreshes a `_lastActivity` timestamp, and silence longer than `WATCHDOG_IDLE_MS = 180_000` is treated as a hang rather than slow encoding [6]. The reasoning is that a live encoder prints statistics several times a second even with demanding codecs [7], which makes the three minute threshold more than 180 times the normal gap between log lines [10]. On trip, the code surfaces a hang message and restarts in a safe mode [8]. The supplied text cuts off before that safe mode is defined, so treat the recovery half as unverified.

Be equally careful with the other two ceilings. The author's headline asserts a 2 GB ceiling and a codec that lies to users alongside the deadlocks [11], and his stated agenda includes processing multi-gigabyte files inside a 32-bit heap using WORKERFS [12] and an explanation of why he ships VP8 when the user asks for VP9 [13]. Those are the claims; the numbers behind them are not in the material we have. A VP8-for-VP9 substitution is still the useful signal: a request that returns success is not the same as a file the user can open.

Around that, the build is deliberately thin. No React, Vue or Svelte, just plain ESM JavaScript and Vite, on the grounds that there is no complex reactive state and the real weight sits in the WASM engines, FFmpeg and Pyodide, so a 100-plus KB framework buys nothing [16]. The site is a multi-page app where each tool owns an `index.html` that Vite turns into its own Rollup entry point, so adding a tool means adding a folder [18]. Imports flow one way, pages to ui to core, and `core/`, `engines/` and `utils/` are forbidden from touching the DOM so they can be tested in Node with Vitest; circular imports in an MPA Rollup build are described as undefined behaviour [19]. The commercial argument for the whole exercise is that the server-side path uploads a half-gigabyte file, waits in a queue, then downloads, and the file sits on someone else's machine throughout [14].

Watch the failure mode, not the demo. If you are removing the server, the two launch-blocking items are an activity watchdog and a hard input size gate, because both of the ceilings here fail quietly rather than loudly.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories