# FFmpeg fits on Vercel. Size isn't the problem

Canonical: https://rendobar.com/blog/ffmpeg-on-vercel-size-limit/
Author: Abdelrahman Essawy
Published: 2026-07-22

---

## Key takeaways

- The ffmpeg-static binary is 76.13 MB. The Vercel Node bundle cap is 250 MB. It fits, and it always did.
- The 50 MB everyone quotes is the AWS zipped upload limit. Gzipped the binary is 28 MB, so it fits under that number too.
- Large functions raised the ceiling to 5 GB, and new Vercel projects are eligible by default.
- The real wall is the 4.5 MB request and response body cap. A video cannot travel through a Vercel Function at all.
- On the Edge runtime it genuinely is impossible. No child_process, and a 1 MB gzipped code cap on Hobby.

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:

```bash
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:

| Asset | Size | Gzipped |
| --- | --- | --- |
| `ffmpeg-linux-x64` | 76.13 MB | 28.00 MB |
| `ffprobe-linux-x64` | 75.98 MB | 27.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.

```json title="vercel.json"
{
  "functions": {
    "app/api/transcode/route.ts": {
      "includeFiles": "node_modules/ffmpeg-static/ffmpeg"
    }
  }
}
```

```ts title="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:

| Plan | Default | Maximum | Extended |
| --- | --- | --- | --- |
| Hobby | 300s | 300s | none |
| Pro | 300s | 800s | 1800s (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:

| Plan | Edge code limit |
| --- | --- |
| Hobby | 1 MB |
| Pro | 2 MB |
| Enterprise | 4 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 the Cloudflare Workers write-up](/blog/ffmpeg-wasm-cloudflare-workers/).

## 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](/blog/best-ffmpeg-api/).

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](/ffmpeg/) 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](/ffmpeg/vercel/) with the full route handler, and the [job reference](/docs/jobs/ffmpeg) covers the parameters.

```ts
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](/pricing/) 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.

| Thing | Value | Source |
| --- | --- | --- |
| `ffmpeg-linux-x64` (release b6.1.1) | 76.13 MB / 28.00 MB gzipped | GitHub release assets |
| Vercel Node bundle cap | 250 MB uncompressed | Vercel limits |
| Large functions ceiling | 5 GB | Vercel limits |
| AWS zipped upload limit | 50 MB | AWS Lambda quotas |
| Edge code cap | 1 / 2 / 4 MB gzipped | Vercel Edge runtime |
| Request and response body cap | 4.5 MB | Vercel limits |
| Max duration | 300s, 800s Pro, 1800s beta | Vercel limits |

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