FFmpeg fits on Vercel. Size isn't the problem

Read the measured numbers behind FFmpeg on Vercel. The binary is 76 MB against a 250 MB cap, and the 4.5 MB body limit is what actually stops you.

8 MIN READ
Share

Search for FFmpeg on Vercel and you get the same answer everywhere. It doesn’t fit. The binary is too big, the bundle limit stops you, go find another platform.

I measured it. The binary fits with 174 MB to spare.

Short version, so you can leave if that’s all you needed. ffmpeg-static ships a 76.13 MB Linux binary against a 250 MB uncompressed bundle cap on Vercel’s Node runtime, and large functions raise that ceiling to 5 GB. It deploys. What actually stops you is the 4.5 MB cap on request and response bodies, which means the video itself can never pass through the function. The Edge runtime is a separate story, and there the impossibility is real.

Every number below was measured on 2026-07-22 against the published artifacts. I’ve said where each one comes from so you can re-run it.

The number everyone quotes is the wrong number

The figure that circulates is 50 MB. It shows up in forum answers, in GitHub discussions, in a widely-shared post arguing Vercel should build a media endpoint because FFmpeg “is not possible due to the package size limitations (50mb)”.

50 MB is a real AWS limit. It is the maximum size of a zipped deployment package uploaded directly to Lambda. It is not the size your code is allowed to be.

The limit that governs your deployment is 250 MB uncompressed, and Vercel states it plainly:

For Vercel Functions, the maximum uncompressed size is 250 MB including layers which are automatically used depending on runtimes.

Two different numbers, two different things, and the wrong one won the argument.

Measuring the binary

ffmpeg-static doesn’t ship the binary inside the npm tarball. It downloads it on install, which is why checking the package size on npm tells you nothing. The version it fetches is pinned in the package metadata:

Terminal window
curl -s https://registry.npmjs.org/ffmpeg-static/5.3.0 \
| jq '."ffmpeg-static"."binary-release-tag"'
# "b6.1.1"

That tag points at a GitHub release. The assets carry their real sizes:

AssetSizeGzipped
ffmpeg-linux-x6476.13 MB28.00 MB
ffprobe-linux-x6475.98 MB27.92 MB

76.13 MB against a 250 MB cap. Ship ffprobe alongside it and you are at 152 MB, still inside.

And note what the gzipped column does to the 50 MB argument. Even if the zipped upload limit were the binding constraint, 28 MB clears it.

So it deploys

Nothing exotic is required. Set the Node runtime, tell Vercel to include the binary, and spawn it.

vercel.json
{
"functions": {
"app/api/transcode/route.ts": {
"includeFiles": "node_modules/ffmpeg-static/ffmpeg"
}
}
}
app/api/transcode/route.ts
import ffmpegPath from "ffmpeg-static";
import { spawn } from "node:child_process";
export const runtime = "nodejs";
export const maxDuration = 300;
export async function POST(req: Request) {
const { input } = await req.json();
const proc = spawn(ffmpegPath!, [
"-i", input, "-c:v", "libx264", "-crf", "23", "/tmp/out.mp4",
]);
await new Promise((resolve) => proc.on("close", resolve));
return Response.json({ ok: true });
}

That builds and runs. The Vercel team published a reference repo for this pattern, vercel-labs/ffmpeg-on-vercel, which they archived on 2025-12-15. Archived is not the same as broken. The approach works.

Then you try to use it for something real.

The wall nobody mentions

Vercel caps the request body and the response body of a function at 4.5 MB. Exceed it and you get 413 FUNCTION_PAYLOAD_TOO_LARGE.

4.5 MB is about eleven seconds of 1080p at a middling bitrate. Less if the footage moves.

This is the constraint that actually ends the pattern, and it survives every workaround aimed at the bundle. You cannot POST a video to your function. You cannot return the transcoded file from your function. Both directions are closed, so the function can never be the thing the media flows through. It can only be the thing that points at where the media lives.

Which means the moment you get past the size question, you are building the same architecture regardless:

  1. The client uploads to object storage directly, using a presigned URL
  2. The function receives a URL, not bytes
  3. FFmpeg reads from that URL and writes back to storage
  4. The function returns a URL, not a file

At that point the binary being in your bundle has stopped buying you anything. It is 76 MB of cold-start weight attached to a process whose real job is coordination.

Duration and the meter

Assume you accept the URL-in, URL-out shape and keep FFmpeg local anyway. Your function now holds open for the entire encode.

The ceilings, from Vercel’s limits page:

PlanDefaultMaximumExtended
Hobby300s300snone
Pro300s800s1800s (beta)

Hobby has no headroom at all. Five minutes is the default and also the wall.

Memory and CPU matter more than the timeout, though. Hobby gives you 2 GB and 1 vCPU. Pro tops out at 4 GB and 2 vCPU. A single vCPU transcoding 1080p H.264 runs somewhere near real time with libx264 at default settings, so a ten minute video is roughly a ten minute function invocation.

You are billed for active CPU time. A long encode is the worst possible shape for that meter, because it is the rare serverless workload that genuinely pins a core for minutes. Fluid compute’s idle-time pricing is a real improvement for functions waiting on a model API. It does nothing for a function that is busy the whole time.

Edge is where “impossible” is true

Everything above is the Node runtime. The Edge runtime is a different machine and the answer flips.

Edge runs on V8 isolates, not containers. The Node modules it supports are async_hooks, events, buffer, assert and util. child_process is not on that list and will not be, because there is no process to fork. Vercel puts it bluntly: you cannot read or write to the filesystem.

So there is nothing to spawn, and nowhere to put a binary if you had one.

The size limit is decisive here too, and this is where the “too big” framing was always correct. Edge code caps are measured after gzip:

PlanEdge code limit
Hobby1 MB
Pro2 MB
Enterprise4 MB

The gzipped binary is 28 MB. That is 28 times over the Hobby ceiling and 7 times over Enterprise. No configuration closes a gap that size.

ffmpeg.wasm is the usual next idea, and it fails for its own reasons. It is a browser tool, the core is large, and it is slow enough that the duration limits arrive quickly. We wrote up the Cloudflare Workers version of that dead end in more detail.

Large functions moved the ceiling anyway

While everyone was repeating the 50 MB number, Vercel raised the real one.

Large functions allow uncompressed bundles up to 5 GB on the Node and Python runtimes. They require fluid compute with active CPU, and Vercel says new projects are eligible by default. Existing projects opt in with VERCEL_SUPPORT_LARGE_FUNCTIONS=1.

5 GB is 65 FFmpeg binaries. Whatever the bundle argument was, it is over.

What to actually do

If your media is small and your volume is low, keep it in the function. Node runtime, includeFiles, presigned URLs on both ends so you never touch the 4.5 MB cap, and a maxDuration you have tested against your longest input. This is a legitimate answer and the reference repo shows the shape. We compared the hosted alternatives to it in our roundup of FFmpeg APIs.

It stops being the answer when any of these become true:

  • Encodes routinely run past your plan’s duration ceiling
  • You are paying for pinned CPU often enough to notice
  • You need codecs, filters or hardware acceleration that a static build doesn’t carry
  • You want the same code path to run from an Edge function, where none of this is possible

Then the work belongs outside the function, and the function becomes what the 4.5 MB cap always implied it should be. It submits a command and gets a URL back.

That is what Rendobar’s FFmpeg API does. You POST the command string you would have typed in a terminal, it runs in a sandboxed container with full codecs and full CPU, and the finished output arrives on a webhook. The call returns in milliseconds, so your function never holds open and never meets the duration ceiling. It works identically from Node and Edge, because from the function’s side it is one fetch. There is a Vercel-specific walkthrough with the full route handler, and the job reference covers the parameters.

import { Rendobar } from "@rendobar/sdk";
const rb = new Rendobar({ apiKey: process.env.RENDOBAR_API_KEY! });
export const runtime = "edge"; // works here too, it's just a fetch
export async function POST(req: Request) {
const { input } = await req.json();
const job = await rb.jobs.create({
type: "ffmpeg",
params: { command: `-i ${input} -c:v libx264 -crf 23 out.mp4` },
});
return Response.json({ jobId: job.id });
}

Every account starts with $5 in credits and no card, which is enough to run a real transcode before deciding anything. The pricing page has the per-job maths.

The numbers, in one place

Measured 2026-07-22. Re-check them before quoting, because platform limits move and this post will age.

ThingValueSource
ffmpeg-linux-x64 (release b6.1.1)76.13 MB / 28.00 MB gzippedGitHub release assets
Vercel Node bundle cap250 MB uncompressedVercel limits
Large functions ceiling5 GBVercel limits
AWS zipped upload limit50 MBAWS Lambda quotas
Edge code cap1 / 2 / 4 MB gzippedVercel Edge runtime
Request and response body cap4.5 MBVercel limits
Max duration300s, 800s Pro, 1800s betaVercel limits

The size limit was never the interesting constraint. The body cap is, and it is the one that decides the architecture.

Tags #ffmpeg#vercel#serverless#aws-lambda
All posts
Share
  1. Parsing ffprobe output is harder than it looks Engineering blog
  2. Why ffmpeg.wasm can't run in a Cloudflare Worker Engineering blog
  3. Best FFmpeg API in 2026, compared Guides for the video API