FFmpeg.wasm Breaks Exactly Where Production Begins
A candid postmortem matches years of open issues — and points to the architecture that actually survives real users.
FFmpeg.wasm demos beautifully. Drop a file, watch the progress bar, download an MP4 — no upload, no server bill, no privacy disclosure. Then real users arrive with 1.8 GB drone footage and a phone from 2021, and you discover what a developer who shipped a browser-based media converter just documented in an unusually honest postmortem: roughly one in ten heavy jobs hanging forever with no error, no log, and no way out except killing the tab.
The details are one person's account, but the failure modes aren't. Every one of them maps onto long-open threads in the ffmpeg.wasm issue tracker — multithreaded jobs that hang forever in Chromium and Safari, out-of-memory crashes near 2 GB, VP9 encodes that die with RuntimeError: unreachable. That's the part worth internalizing: these aren't bugs waiting on a patch release. They're structural properties of running a 25-year-old C codebase inside a browser sandbox, and they should change how you architect anything media-shaped on the client.
The deadlock is the architecture
The hangs come from how Emscripten fakes pthreads. There are no real threads in a browser tab — pthread_create gets mapped onto Web Workers, and spawning a Worker requires yielding to the event loop. Emscripten papers over this with a pre-spawned Worker pool, typically sized to navigator.hardwareConcurrency. If FFmpeg's decoder and encoder both ask for more threads than the pool holds, pthread_create blocks waiting for a Worker that can never be created, because the code that would create it is the code that's blocked. Emscripten's own docs describe exactly this deadlock; the ffmpeg.wasm tracker has it filed as jobs that "hang forever without error" on the multithreaded core — in Chromium and Safari, but not Firefox, which tells you how sensitive the failure is to scheduler details you don't control.
You can't configure your way out. The postmortem author's answer was supervision: a watchdog that treats 180 seconds of encoder silence as death and retries down a ladder — multithreaded, then -threads 1, then single-threaded plus a downscale. Even the timeout needed tuning, because x265 on a slow machine can legitimately go quiet for over a minute mid-encode. That's the correct production pattern, and it's telling that it's the same one you'd use for a flaky remote service. The multithreaded build buys you roughly 2x throughput by the project's own numbers, and it costs you a supervisor process, retry logic, and the cross-origin isolation headers (COOP/COEP) that SharedArrayBuffer demands — which also quietly break third-party embeds elsewhere on the page. For most apps that trade is upside down. Ship single-threaded by default; treat core-mt as an optimization you enable only where you can babysit it.
The 2 GB ceiling has an expiry date — just not here
FFmpeg.wasm's FAQ is blunt about the 2 GB file limit, and it's really a wasm32 address-space limit colliding with a lazy default: MEMFS, the standard Emscripten filesystem, copies your entire input into the WASM heap. A 1.8 GB source file plus encoder buffers blows the budget before the first frame is written; mobile browsers fall over far earlier, around 500 MB in the author's testing. The workaround is WORKERFS, which mounts the browser File object and reads it in chunks instead of inhaling it — at the cost of losing multithreading, which by then you've probably abandoned anyway. A ~128 MB threshold for switching filesystems is a sane default to steal.
The real fix is Memory64, which shipped in Chrome unflagged in early 2025 and landed in Wasm 3.0. But nobody has rebuilt the popular ffmpeg.wasm distribution on it, Safari trails, and the project's release cadence doesn't inspire confidence that it's imminent: the last @ffmpeg/core release was April 2025, sixteen months ago, with close to 400 issues open. If your users have 4 GB files today, the answer is a server, not a roadmap.
When the codec menu lies
The ugliest finding is what happens with VP9. libvpx-vp9 in the WASM build fails in every thread configuration — TypeErrors, hangs, or a core crash — even with the flag incantations (-row-mt 0, -tile-columns 0, -frame-parallel 0) that tame it in native builds. The tracker independently confirms VP9 transcodes failing with memory faults. This is what happens to codec libraries with hand-rolled threading and assembly assumptions when they meet emulated pthreads: they don't degrade, they detonate.
The author's response was to silently encode VP8 when users pick "WebM · VP9." I understand the reasoning — the user wants a file that plays, and VP8-plus-Opus in WebM plays everywhere VP9 does. I still think it's the wrong call. Developers pick VP9 for compression efficiency and pipeline compatibility, and a tool that hands back a different bitstream than its UI promised will eventually burn someone's transcoding farm or CI check in a way that's miserable to debug. The honest version costs one line of UI copy: "VP9 unavailable in software mode, using VP8." Say it.
Ship the ladder, not the library
The pattern hiding in this postmortem is the right architecture for browser media in 2026, and notably ffmpeg.wasm isn't at the top of it. WebCodecs — now in all three engines — gives you hardware-accelerated encode and decode, and it's where VP9 actually worked in this app, because it never touches WASM. FFmpeg.wasm's real job is the layer below: the compatibility shim for the container remuxing, filter graphs, and long-tail formats WebCodecs doesn't cover, running single-threaded, under a watchdog, with WORKERFS past ~128 MB. Above 2 GB, or for anything latency-sensitive, you're back to a server and that's fine — client-side conversion is a privacy and cost feature, not an identity.
FFmpeg.wasm is a genuinely impressive artifact that gets marketed — mostly by its 17k GitHub stars — as infrastructure. It's not. It's the fallback tier of your pipeline, and every production war story, this one included, is from someone who found that out after launch. Cheaper to believe it now.
Sources & further reading
- FFmpeg.wasm in production: deadlocks, a 2 GB ceiling and a codec that lies to users — dev.to
- ffmpeg.exec hangs forever without error when using the multithreaded version — github.com
- AV1 and VP9 transcoding failure — github.com
- ffmpeg.wasm FAQ — ffmpegwasm.netlify.app
- Pthreads support — emscripten.org
Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.
Discussion 0
No comments yet
Be the first to weigh in.