How to Process Video in Firebase (Cloud Functions Alternative with FFmpeg API)

You wire up a Storage trigger, npm install ffmpeg-static, and deploy. A 20-second test clip transcodes fine. Then a real user uploads a 12-minute 1080p screen recording, the function dies at 540 seconds, and Cloud Functions retries it three more times before you notice the bill.
Quick answer: Firebase video processing fails inside Cloud Functions for Firebase, Google's serverless runtime built on Google Cloud Functions, because Storage-triggered functions are capped at 540 seconds (9 minutes) in both 1st and 2nd gen, and their/tmpdirectory is an in-memory tmpfs that eats the same RAM allocation FFmpeg needs. Bundlingffmpeg-staticand shelling out toffmpeg -i input.mov -c:v libx264 -crf 24 output.mp4does work, but only for clips short enough to finish inside that window. The pattern that holds up in production is to keep the Firebase Storage trigger, have it pass a signed URL to a hosted FFmpeg API like FFmpeg Micro, and write the finished file back to Storage from a webhook handler.
Why Firebase video processing breaks inside Cloud Functions
Conventional wisdom says Cloud Functions is too small for video and you should upgrade to Cloud Run. That's half right. The real blocker isn't raw size, it's that a Storage trigger is an event-driven function, and Google caps every event-driven function at 540 seconds no matter which generation you deploy to. Moving to 2nd gen buys you 32 GB of RAM and 8 vCPU, and the clock still stops at nine minutes.
| Constraint | 1st gen | 2nd gen | Why it hurts video |
|---|---|---|---|
| Timeout (event-driven) | 540s max | 540s max | A 12-min 1080p transcode routinely exceeds it |
| Timeout (HTTP) | 540s max | 3600s max | Storage triggers aren't HTTP, so this doesn't apply |
| Memory | 256 MB default, 8 GB max | 32 GB max | Source file + output file live in RAM |
| CPU | Tied to memory tier | Up to 8 vCPU | 256 MB gets you roughly 400 MHz |
| Writable disk | `/tmp` only, in-memory | `/tmp` only, in-memory | Every byte you download counts twice |
That last row is the one that surprises people. On Cloud Functions the filesystem is read-only except for /tmp, and /tmp is a tmpfs. Download a 190 MB source video and write a 60 MB output, and you've spent 250 MB of your allocation before libx264 allocates a single frame buffer. This is the same failure mode that kills FFmpeg on AWS Lambda, just with a different error message.
Then there's the binary itself. The ffmpeg-static Linux x64 build is about 78 MB, which pushes your deploy near the 1st gen 100 MB compressed source limit and drags out every cold start. If you want the full autopsy on that path, we covered it in FFmpeg on Firebase Cloud Functions: Why It Breaks.
And the math is bad even when it works. A 2 GB / 2.4 GHz function that runs for 500 seconds costs roughly $0.0145 in GB-seconds and GHz-seconds. Multiply that by an automatic retry loop on a job that never finishes, and you're paying full price for zero output.
How to run Firebase video processing without hitting the 540-second timeout
Don't make FFmpeg fit in a 540-second box. Take FFmpeg out of the box.
Firebase is genuinely good at the parts around video: object events, signed URLs, Firestore state, auth rules. It's bad at pinning a CPU for eleven minutes. So the function becomes a dispatcher. It fires in about 200 milliseconds, hands off a URL, and exits. The encode happens on infrastructure built for it, and a webhook writes the result back.
Firebase's official extension catalog covers image resizing, not video transcoding, so there's no drop-in to install here. You wire it yourself, and it's about 40 lines.
1. Trigger on the Storage upload
The trigger has one job: notice a new video, mint a read URL for it, and hand that URL to the encoder. It never touches the file bytes.
const { onObjectFinalized } = require("firebase-functions/v2/storage");
const { getStorage } = require("firebase-admin/storage");
exports.onVideoUpload = onObjectFinalized(
{ region: "us-central1", memory: "256MiB", retry: false },
async (event) => {
const { bucket, name, contentType } = event.data;
if (!contentType?.startsWith("video/")) return;
if (name.startsWith("processed/")) return; // don't re-trigger on our own output
const [signedUrl] = await getStorage()
.bucket(bucket)
.file(name)
.getSignedUrl({ action: "read", expires: Date.now() + 6 * 60 * 60 * 1000 });
await submitJob(signedUrl, name);
}
);
Two details matter. retry: false stops a failed dispatch from replaying forever, and the processed/ guard stops the function from firing on the file it just wrote. Both are cheap to add now and painful to debug later.
2. Submit the job with a webhook
Submitting the job is a single POST that returns a job ID immediately instead of holding the connection open for the length of the encode. That's the whole reason the function can exit in milliseconds.
async function submitJob(inputUrl, sourcePath) {
const res = await fetch("https://api.ffmpeg-micro.com/v1/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
input_url: inputUrl,
webhook_url: "https://us-central1-my-app.cloudfunctions.net/onJobComplete",
metadata: { source_path: sourcePath },
}),
});
return res.json(); // { "id": "job_...", "status": "queued" }
}
Field names differ by operation (compress, caption, watermark, thumbnail), so pull the exact payload for your job from the docs. The shape is always the same: submit a job, get an ID back immediately, then poll or wait for the webhook.
3. Write the output back to Storage
The finished job arrives as an HTTP POST carrying a URL to the output file plus whatever metadata you attached at submit time. Your handler downloads that file and saves it under a separate prefix.
const { onRequest } = require("firebase-functions/v2/https");
exports.onJobComplete = onRequest(async (req, res) => {
const { status, output_url, metadata } = req.body;
if (status !== "completed") return res.status(200).send("ack");
const file = await fetch(output_url);
const buffer = Buffer.from(await file.arrayBuffer());
await getStorage()
.bucket()
.file(`processed/${metadata.source_path}`)
.save(buffer, { contentType: "video/mp4" });
res.status(200).send("ok");
});
The webhook handler is an HTTP function, not an event function, so it isn't fighting a 540-second clock. It's also doing nothing but a download and a write, which finishes in seconds. Verify the webhook signature before trusting the payload, same as you would with a Stripe hook.
FFmpeg CLI vs one API call
The same compress job looks completely different depending on where it runs. On your laptop, a standard web-delivery encode is one command:
ffmpeg -i input.mov -c:v libx264 -preset veryfast -crf 24 \
-vf "scale=-2:1080" -c:a aac -b:a 128k -movflags +faststart output.mp4
That command needs a machine with the binary installed, enough disk for both files, and a process that can stay alive for as long as the encode takes. Inside a Storage trigger you have none of the three.
| FFmpeg in Cloud Functions | Hosted FFmpeg API | |
|---|---|---|
| Binary to ship | ~78 MB in your bundle | none |
| Hard time limit | 540s, then killed | none in your function |
| Working disk | in-memory `/tmp` | not your problem |
| Cold start impact | seconds, every scale-up | ~200ms dispatch |
| Failure mode | timeout plus retry storm | job status + webhook |
The mechanism change is what matters: your function stops doing the work and starts scheduling it. That's the same move we used for Supabase Edge Functions, where the ceiling is even lower.
Common pitfalls in Firebase video processing
Most of the failures in this setup aren't FFmpeg failures. They're plumbing, and they show up days after the happy path works.
Signed URLs expire mid-job. A five-minute expiry looks fine in testing and dies when a job sits in a queue behind a backlog. Six hours costs you nothing and removes an entire class of "input not readable" errors.
Writing output to the same prefix creates a loop. onObjectFinalized fires on every new object, including the one your webhook just saved. Without a prefix guard you get a recursion that transcodes its own output until you notice. Use a separate processed/ prefix or a separate bucket.
Leaving retries on doubles the damage. Event-driven functions retry on failure by default in some configurations. If FFmpeg times out at 540 seconds, the retry also times out at 540 seconds, and you pay for both. Set retry: false and handle failures explicitly.
Assuming 2nd gen fixes the timeout is the single most common wrong turn. 2nd gen fixes memory and CPU. Storage triggers stay at 540 seconds because they're event-driven, and the 3600-second number applies to HTTP functions only.
Zero-byte and partial objects poison the queue. Resumable uploads can produce events for objects that aren't fully written. Check event.data.size before dispatching so you don't submit an empty file and get back a decode error.
When you shouldn't offload
Offloading Firebase video processing is the right default, not a universal rule. If every clip is under 15 seconds at 720p and your current function already finishes in 40 seconds, leave it alone. There's no prize for adding a network hop to something that works.
If what you actually need is adaptive-bitrate streaming with a player and a CDN, a hosted FFmpeg API is the wrong shape. Reach for a managed video delivery platform instead: it handles the renditions, the manifests, and the edge caching for you. FFmpeg Micro is built for processing, not delivery: transcode, caption, watermark, compose, extract.
And if compliance requires that media never leave your own GCP project, run Cloud Run with a mounted disk instead. It's more work, and it's the right call for that constraint.
FAQ
Can Firebase Cloud Functions run FFmpeg at all?
Firebase Cloud Functions can run FFmpeg, technically. You bundle ffmpeg-static, shell out to it, and short clips process fine. It breaks in production because Storage-triggered functions are killed at 540 seconds and /tmp is in-memory, so both the input and output files consume your RAM allocation.
What's the actual timeout for a Firebase Storage trigger?
A Firebase Storage trigger is capped at 540 seconds, or 9 minutes, in both 1st gen and 2nd gen Cloud Functions. The 3600-second maximum you may have read about applies only to 2nd gen HTTP functions, and a Storage trigger is not an HTTP function.
How do I handle Firebase video processing without hitting the timeout?
Trigger on onObjectFinalized, generate a signed read URL for the uploaded object, POST it to a video processing API with a webhook URL, and return. The function finishes in a couple hundred milliseconds. A separate HTTP function receives the webhook and saves the output back to Firebase Storage.
Does moving to Cloud Run solve this?
Cloud Run solves the timeout, since it allows up to 60 minutes. You then own the container image, the FFmpeg version, the disk, the concurrency tuning, and the scale-to-zero cold starts. We broke down that trade in FFmpeg in the cloud vs running FFmpeg yourself.
Can I do this without writing a Cloud Function?
You can skip Firebase Functions entirely. If your uploads already flow through n8n, Make, or Zapier, call the API directly from the workflow. The same job also runs from an AI agent through the MCP server.
The free tier is enough to run a real 12-minute upload end to end and see the webhook land, which is the only way to know your prefix guards and signed URLs are right. Sign up free and point your first Storage trigger at it.
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.
You might also like

FFmpeg Concat Different Resolutions: Normalize, Then Copy
FFmpeg concat different resolutions fails with -c copy. Two fixes that work: normalize then demux, or a one-pass filter_complex concat, plus how to pick.

Normalize audio loudness across a video library
Normalize audio loudness across a video library with FFmpeg loudnorm two-pass, EBU R128 targets, and one API call. Real commands, LUFS targets, and pitfalls.

The NCA Toolkit is free. Self-hosting it isn't cheap.
What self-hosting the NCA Toolkit costs: Cloud Run's 5-minute wall, worker timeouts, bucket setup, and CVE patching, plus a side-by-side API comparison.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free