FFmpeg.wasm: Run FFmpeg in the Browser Without a Server

April 13, 2026 · RenderIO

What ffmpeg.wasm actually is

ffmpeg.wasm is FFmpeg compiled to WebAssembly. That's the whole idea.

The ffmpeg.wasm project takes FFmpeg's C source code and runs it through Emscripten, a compiler that targets WebAssembly instead of native machine code. The result is an @ffmpeg/core package containing the WASM binary, and an @ffmpeg/ffmpeg wrapper that handles loading it, marshaling data in and out, and exposing a JavaScript API.

When your user opens a browser tab, the WASM binary loads into their browser's sandbox. FFmpeg runs there, inside the tab, using their CPU. Files never leave their machine. No server involved.

That last part is the main appeal: files never leave the browser, so there's no upload, no server logs, and no third-party handling your users' data. For tools built around that promise—a no-upload video converter, an in-browser audio editor—ffmpeg.wasm is the technically correct solution.

But it comes with real limits, and the official docs are honest about them. Before you build a production pipeline around it, you should understand what you're working with.

Installing and basic usage

npm install @ffmpeg/ffmpeg @ffmpeg/core

The @ffmpeg/ffmpeg package is the JavaScript wrapper. @ffmpeg/core ships the WebAssembly binary (currently based on FFmpeg 5.1.4, compiled with Emscripten 3.1.40).

Here's a minimal example that converts MP4 to WebM:

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

const ffmpeg = new FFmpeg();

// Load the WASM binary — this can take a few seconds on first run
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
await ffmpeg.load({
  coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
  wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});

// Write the input file to ffmpeg.wasm's virtual filesystem
await ffmpeg.writeFile('input.mp4', await fetchFile('/path/to/video.mp4'));

// Run the conversion — same flags as the command line
await ffmpeg.exec(['-i', 'input.mp4', '-c:v', 'libvpx-vp9', 'output.webm']);

// Read the result back out
const data = await ffmpeg.readFile('output.webm');
const url = URL.createObjectURL(new Blob([data.buffer], { type: 'video/webm' }));

The command syntax is identical to what you'd run on the command line, split into an array. If you know FFmpeg, you know ffmpeg.wasm.

The load() call is slower than it looks. The WASM binary is several megabytes, and it needs to parse and JIT-compile before any processing starts. On a fast connection this takes 2-5 seconds. On mobile or slow connections, noticeably longer.

The SharedArrayBuffer requirement for multithreading

By default, ffmpeg.wasm runs single-threaded. One thread. The JavaScript event loop blocks while FFmpeg processes your file.

There's a multi-threaded version, but it requires SharedArrayBuffer, and that's where things get complicated. SharedArrayBuffer was disabled across browsers after Spectre and only re-enabled when pages meet cross-origin isolation requirements:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

These two headers must be set at the server level for every page using the multi-threaded version. That's fine if you control the server, but it breaks <iframe> embeds, some third-party scripts, and certain CDN configurations.

The official FAQ is direct about this: the multi-threaded version is "unstable" and "might not work for many cases." In practice, most production uses of ffmpeg.wasm stick with the single-threaded version and work around the performance ceiling.

Why ffmpeg.wasm is slow

From the official FAQ: "WebAssembly is still a lot slower than native."

That's not a temporary bug. WebAssembly can't access GPU hardware for video encoding, can't use native SIMD as efficiently as a compiled binary, and pays overhead at the JavaScript/WASM boundary. The multi-threaded version gets "around 2x" speed versus single-threaded. Native FFmpeg on the same hardware is easily 10-50x faster depending on the codec.

In concrete terms: a 30-second 1080p MP4 converting to H.264 might take 15-20 seconds in native FFmpeg. The same job in single-threaded ffmpeg.wasm can take several minutes. Transcoding to H.265 or VP9 is worse.

For small files (under ~50MB) and simple operations like audio conversion, trimming, or thumbnail extraction, the performance is tolerable. For anything involving long video files or computationally heavy codecs, users will be sitting at a progress bar for a long time.

The 2GB file size ceiling

Hard limit. WebAssembly's linear memory model caps addressable space at 2GB. Files larger than this can't be loaded into the virtual filesystem at all. writeFile() will fail before processing even starts.

This ceiling might move to 4GB in a future version of the WebAssembly spec, but for now it's fixed. If your users deal with uncompressed video, raw footage, or long recordings, this will hit you.

The limit also applies to total memory usage, not just input file size. Processing a 1.5GB file might require enough working memory to push you over 2GB during encoding.

What ffmpeg.wasm is genuinely good for

Not everything. But for the right use cases, it's the right tool.

Browser-based file tools. If you're building a no-upload converter, an audio normalizer, or a video trimmer where the whole pitch is "your files never leave your device," ffmpeg.wasm is the technically correct solution. You can't fake client-side processing. Browsers don't have hidden servers behind them. For a comparison of browser-based tools versus server-based approaches, see FFmpeg online tools explained.

Simple audio conversions. Converting an MP3 to WAV, or an AAC to OGG, runs quickly even in single-threaded mode. Audio files are typically small, codec requirements are lightweight, and users don't expect instant results.

Thumbnail extraction. Pulling a single frame from a video is one of the faster operations in FFmpeg; even WASM handles it in a few seconds for most files.

Offline-capable apps. Progressive web apps that need to work without an internet connection. Once the WASM binary is loaded and cached, processing continues offline.

Demos and developer tooling. If you're building something to show developers what FFmpeg commands do, or a playground for experimenting, the browser-native approach makes deployment trivially simple.

Where it breaks down for production use

Production video processing has different requirements. Concurrency, reliability, file size handling, and speed all matter more than they do in a one-off demo.

Batch processing. You can run multiple ffmpeg.wasm jobs in sequence, but not in parallel. The WASM runtime isn't designed for concurrent execution in separate workers. Each job loads, processes, and cleans up one at a time. For anything involving many files, this becomes a bottleneck fast.

Large files. A user uploading a 90-minute lecture recording, a raw 4K clip, or any file over a few hundred MB will have a bad time. Processing might take longer than the browser's script timeout in some cases.

Mobile devices. Mobile browsers have aggressive memory limits and power management that can kill long-running WASM processes. A job that works fine on desktop might crash on mobile.

No streaming. ffmpeg.wasm requires the entire input file to be loaded into memory before processing starts. You can't stream input or output. Everything goes through the virtual filesystem: full file in, full file out.

RTSP and network protocols. The official docs confirm RTSP is not supported. WebAssembly lacks the socket primitives required. Live video processing or any protocol-level networking is off the table.

Here's a comparison that summarizes the tradeoff:

ffmpeg.wasmRenderIO API
File size limit2 GB hard ceilingNo practical limit
Speed10-50x slower than nativeNative FFmpeg performance
Parallel jobsSingle-threaded (MT requires extra headers)Concurrent cloud workers
GPU encodingNot supportedAvailable (NVENC)
User privacyFiles stay in browserFiles sent to API
Setup complexitynpm install, COOP/COEP headersOne HTTP call
Mobile reliabilityMemory-limited, can crashRuns server-side
RTSP / streamingNot supportedSupported

When to move to an API

The clearest signal: when your users are complaining that processing is slow.

If you've shipped a browser-based tool and your feedback is "this takes forever" or "it crashed on my phone," that's not a fixable problem with more JavaScript. The ceiling is architectural.

The RenderIO API works differently. You send an HTTP request with your FFmpeg command and the URL of your input file. The API runs FFmpeg on cloud infrastructure at native speed, stores the output in cloud storage, and gives you a URL to download the result.

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "X-API-KEY: ffsk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "ffmpeg_command": "-i {{in_video}} -c:v libx264 -preset fast {{out_video}}",
    "input_files": { "in_video": "https://example.com/input.mp4" },
    "output_files": { "out_video": "output.mp4" }
  }'

The syntax maps directly to what you'd use with ffmpeg.wasm. If you know the FFmpeg command, you can migrate in an afternoon.

The tradeoff is that files go to the API. For sensitive content, that matters and browser-side processing is the right answer. For most product use cases (video editing apps, content pipelines, SaaS tools), sending files to an API is normal and the performance improvement is significant.

For a more detailed breakdown of when it makes sense to run FFmpeg in the cloud versus managing your own setup, see self-hosted vs hosted FFmpeg compared.

Using both in the same product

Not a bad approach for some products.

Use ffmpeg.wasm for the preview flow: quick thumbnails, a waveform visualization, or a fast trim before the user commits. Then use an API for the final render at full quality. The user gets fast feedback from the browser and high-quality output from the cloud.

You could also let users choose: "Process locally (slower, private)" versus "Process in cloud (faster, requires upload)." Some users care deeply about file privacy. Most don't. Giving both options is an honest way to handle it.

The practical checklist

Before committing to ffmpeg.wasm for a project, run through this:

  • Are your input files reliably under 500MB?

  • Can you serve the required COOP/COEP headers if you need multithreading?

  • Will users be on desktop browsers with good CPU performance?

  • Is client-side processing a core privacy requirement, not just a nice-to-have?

  • Do you need only simple operations (convert, trim, thumbnail)?

If you answered yes to most of those, ffmpeg.wasm is worth trying. If you're unsure about file sizes, need batch processing, or are building a mobile-first experience, start with an API instead.

For a full walkthrough of the RenderIO API with more code examples and common FFmpeg recipes, see the complete FFmpeg API guide. If you need quick reference commands for specific tasks like extracting frames or compressing video, those posts have copy-paste examples. And if you're exploring the broader landscape of ways to run FFmpeg in the cloud without managing a server, that post covers your options without the server maintenance headache.

FAQ

Can ffmpeg.wasm process live video or RTSP streams?

No. ffmpeg.wasm runs entirely in the browser's WebAssembly sandbox, which has no access to raw sockets. RTSP and other network protocols require system-level networking that WebAssembly can't provide. For live video, you need a server-side solution.

Does ffmpeg.wasm work on mobile browsers?

Technically yes, but unreliably. Mobile browsers impose aggressive memory limits and power management that can kill long-running WASM processes mid-job. A 200MB video conversion that works fine on a desktop Chrome may crash on iOS Safari. If your users are primarily on mobile, start with an API instead.

What's the maximum file size ffmpeg.wasm can handle?

2GB, hard. WebAssembly's linear memory model caps addressable space there. In practice, large files also consume significant working memory during encoding, so you may hit issues well below 2GB on memory-constrained devices.

How do I enable multithreading in ffmpeg.wasm?

You need to serve your app with two HTTP headers: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. These enable SharedArrayBuffer, which the multi-threaded WASM build requires. Be aware that these headers can break <iframe> embeds and some third-party scripts. The official ffmpeg.wasm docs recommend the single-threaded version for most production use.

Is there a free tier for the RenderIO API?

There's no permanent free tier, but there is a free trial with no credit card required. Paid plans start at $12/mo for 500 commands. For context: a typical audio extraction or video conversion job counts as one command, so 500 commands covers a meaningful amount of testing and light production use.