supabase ffmpegsupabase video processing

Supabase Edge Functions Can't Run FFmpeg: Offload It to a Video API

·Javid Jamae·6 min read
Supabase Edge Functions Can't Run FFmpeg: Offload It to a Video API

Why Supabase Edge Functions can't run FFmpeg

You wrote a Supabase Edge Function to transcode an upload, called Deno.Command("ffmpeg", ...), and got nothing back. The function didn't crash your logic. The runtime just refused to spawn the process. This is the wall every indie dev on Supabase hits, and it shows up over and over in GitHub discussions (supabase/supabase #27280, #3773, #36800, and storage #433) with no first-party answer.

The short version: Edge Functions run on Deno in a V8 isolate, not a container with a shell. There's no ffmpeg binary on the box, and even if there were, you can't launch it.

No binary, no shell, no subprocess

FFmpeg is a native command-line program. To use it the normal way, you shell out to it: your code spawns a child process, passes arguments, and reads the output file. That model needs three things Edge Functions don't give you:

  • A filesystem with the ffmpeg executable installed. Edge Functions ship a minimal Deno runtime, not a Debian image you can apt install ffmpeg into.
  • Permission to spawn subprocesses. Deno.Command and the older Deno.run are blocked in the hosted isolate. Locally with supabase functions serve it may look like it works, then fails once deployed.
  • Enough memory and wall-clock time to actually encode. Edge Functions cap memory (256MB on the free tier) and are built for fast request/response work, not multi-second CPU-bound jobs.

So the classic Node.js pattern of fluent-ffmpeg wrapping a local binary is a non-starter here. It's the same class of problem we covered for Cloudflare Workers and AWS Lambda: the serverless sandbox won't host a native media toolchain.

Why ffmpeg.wasm doesn't rescue you

The obvious next idea is ffmpeg.wasm, the WebAssembly build that runs without a native binary. It imports fine in Deno, so it feels like the fix. It isn't, for two reasons.

First, memory. ffmpeg.wasm loads a multi-megabyte core and decodes frames into a WASM heap. A short 1080p clip can push past the 256MB isolate limit and get the function killed mid-job. Second, CPU. WASM transcoding runs several times slower than a native binary, and Edge Functions aren't sized for long CPU-bound loops. Even when a tiny clip squeaks through, anything real times out. We broke down exactly where browser-side and WASM encoding falls apart in ffmpeg.wasm vs a hosted FFmpeg API.

This is why paid workarounds like Rendobar and BuildShip keep coming up in the same threads. The demand is real and Supabase itself doesn't solve the media step.

Offload FFmpeg to a video API

Keep the Edge Function doing what it's good at, coordinating, and send the actual encoding to a service built for it. FFmpeg Micro is the FFmpeg API: cloud video processing with no servers to run and no FFmpeg to install. Your Edge Function makes one API call, the job runs off-platform, and a webhook writes the result back into Supabase Storage.

The flow has three moving parts:

  1. An Edge Function that receives the upload event and submits a job.
  2. FFmpeg Micro processes the video and calls your webhook when it's done.
  3. A second Edge Function (the webhook handler) downloads the output and stores it in a Supabase Storage bucket.

Step 1: submit the job from your Edge Function

Instead of spawning ffmpeg, POST a job to api.ffmpeg-micro.com. Pass the input URL (a signed Supabase Storage URL works), the operation you want, and a webhook URL pointing at your second function.

// supabase/functions/transcode/index.ts
Deno.serve(async (req) => {
  const { videoUrl, recordId } = await req.json();

  const res = await fetch("https://api.ffmpeg-micro.com/v1/jobs", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${Deno.env.get("FFMPEG_MICRO_KEY")}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      input: videoUrl,
      operation: "transcode",
      format: "mp4",
      webhook: `${Deno.env.get("SUPABASE_URL")}/functions/v1/on-complete`,
      metadata: { recordId },
    }),
  });

  return new Response(JSON.stringify(await res.json()), {
    headers: { "Content-Type": "application/json" },
  });
});

The function returns in milliseconds because it isn't doing the encoding. No memory pressure, no timeout, no blocked subprocess. Check the docs for the exact request shape and the full list of operations (compress, captions, watermark, thumbnails, audio extraction).

Step 2: store the result on the webhook

When the job finishes, FFmpeg Micro calls your webhook with a download URL for the output. The handler streams it into Supabase Storage.

// supabase/functions/on-complete/index.ts
import { createClient } from "npm:@supabase/supabase-js";

Deno.serve(async (req) => {
  const { output, metadata } = await req.json();
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
  );

  const file = await fetch(output.url).then((r) => r.blob());
  await supabase.storage
    .from("processed")
    .upload(`${metadata.recordId}.mp4`, file, { upsert: true });

  return new Response("ok");
});

Now the heavy lifting happens off your isolate, and the only thing crossing back through Supabase is a finished file.

Manual FFmpeg vs a hosted API on Supabase

ApproachWorks on Edge Functions?Handles a 1080p clip?Setup cost
`Deno.Command("ffmpeg")`No, subprocess blockedNoN/A
`ffmpeg.wasm` in the isolateImports, then OOMsNo, hits 256MB / CPU capMedium
Separate container (Fly, Render)YesYesHigh, you run a server
FFmpeg Micro API + webhookYesYesOne API call

The container route works, but now you're back to running and scaling a media microservice, which is the thing serverless was supposed to remove.

Common pitfalls

  • Testing only with supabase functions serve. Local Deno may allow subprocess spawns that the deployed runtime blocks. Always test against the deployed function before you trust it.
  • Doing the encode synchronously. Even with a WASM build, blocking the request until a video finishes will hit the wall-clock limit. Submit a job and let a webhook finish the work.
  • Uploading with the anon key. Writing to a Storage bucket from the webhook needs the service role key, and that function should verify the request came from your job provider.
  • Forgetting signed URLs. If your source bucket is private, generate a signed URL so the API can read the input.

FAQ

Can you install FFmpeg in a Supabase Edge Function?

No. Edge Functions run in a Deno V8 isolate with no shell and no package manager, so there's nowhere to install a native ffmpeg binary and no way to spawn it with Deno.Command.

Does ffmpeg.wasm work in Supabase Edge Functions?

It imports, but real jobs fail. The WASM core plus decoded frames overrun the 256MB memory limit, and WASM transcoding is too CPU-heavy for the function's time budget.

Where does the processed video get stored?

Wherever you want. In this pattern the webhook handler streams the output straight into a Supabase Storage bucket, so it lands right next to your other app data.

Wire up the two functions above and your Supabase app processes video without a server, a binary, or a blown memory limit. Sign up free and grab an API key, then follow the docs to submit your first job.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free