Process video from an S3 bucket with FFmpeg

Process video stored in S3 past Lambda's 900-second limit. One FFmpeg job on our API ran for 1,002 seconds end to end and billed $1.02 for the run.

Share

The footage is already in S3. The obvious pipeline is an S3 event, a Lambda function with an FFmpeg layer, and the result written back. It works for short clips, and it stops working at a size and a duration that have nothing to do with FFmpeg.

Short version. Lambda ends every run at 900 seconds and gives it at most 10,240 MB of scratch disk. A job that reads the object from the bucket has neither wall. One we ran lasted 1,002 seconds start to finish, completed normally and billed $1.02. The ceiling that applies instead is the plan’s job limit, 1 hour on Free and 9 hours on Pro.

AWS LAMBDAS3 bucketraw/clip.mp4GETfunction/tmp copyffmpeg900 s wall/tmp up to 10,240 MByou hold the keyA JOB WITH A STORAGE INPUTS3 bucketraw/clip.mp4signed linkstorage://media/raw/clip.mp4ffmpeg, to the plan job limitdeliveryS3 bucketexports/The link reaches one object and nothing else. No key leaves your side, and no 900 s wall applies.Read only connections admit sources and refuse deliveries, for a bucket you never want written to.
The same object, processed two ways. The function copies it and races a clock, the job reads it through a link to that one file.

The limits a Lambda function runs into

Lambda is built for short tasks, and its quotas say so. These are the ones a video pipeline meets first, from AWS’s own quota page:

LimitValueWhat it means for video
Function timeout900 secondsThe encode, the download and the upload all fit inside 15 minutes
/tmp storage512 MB to 10,240 MBInput and output both have to fit at once
Memory128 MB to 10,240 MBCPU is allocated in proportion, one vCPU at 1,769 MB
Synchronous payload6 MBThe video can’t travel in the request or the response
Network bandwidth625 Mbps per execution environmentPulling a large object eats into the 900 seconds

The timeout is the one that ends the conversation. AWS’s own answer to a question about processing a 40 GB video with Lambda was to use a different service. The AWS media blog’s well-known pattern streams the object through FFmpeg in memory to avoid /tmp, which helps with the disk and does nothing for the clock.

A run past 900 seconds

To show the difference with a real run rather than a quota table, we submitted one FFmpeg job to the Rendobar API built to last 1,000 seconds:

Terminal window
ffmpeg -re -stream_loop -1 -i https://cdn.rendobar.com/assets/examples/sample.mp4 \
-t 1000 -c:v libx264 -preset veryfast -crf 28 -c:a aac -b:a 96k out.mp4

-re reads the input at its own frame rate and -stream_loop -1 repeats it, so -t 1000 makes the run last 1,000 seconds regardless of how fast the machine encodes. The point being measured is how long a job may run, not how fast the encoder is, and a CPU-bound encode would have landed anywhere from nine minutes to twenty depending on the machine.

StepTime
Waiting to start0.8 s
Download input40 ms
FFmpeg1,000,274 ms
Upload the 57.3 MB output2,175 ms
Start to finish1,002 s

It completed. The job ran 102 seconds past the point where a Lambda function would have been stopped, the output landed, and the run billed $1.02, which works out to about a tenth of a cent per second.

Horizontal bar chart on a log scale. Supabase Edge Function wall clock is 150 seconds on Free and 400 seconds on paid plans. AWS Lambda's function timeout is 15 minutes. The Rendobar job we ran lasted 16 minutes 42 seconds. Rendobar's job limit is 1 hour on Free and 9 hours on Pro.
Runtime ceilings on one run. The 16 minute 42 second job sits past the serverless limits and well inside a plan's job limit.

Reading the object from S3

That run read a public URL. Reading from your bucket is the same job with a different input. Connect the bucket once, then pass the object as storage://<connection>/<key>:

Read raw footage from a connected bucket and deliver the encode back. media is the connection's ID.

job.ts
import { createClient } from "@rendobar/sdk";
const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
const job = await rb.jobs.create({
type: "ffmpeg",
inputs: { source: "storage://media/raw/interview.mov" },
params: {
command: "ffmpeg -i source -c:v libx264 -preset slow -crf 20 -c:a aac -movflags +faststart out.mp4",
},
destinations: ["storage://media/exports"],
});
console.log(job.id);

Install with npm i @rendobar/sdk. jobs.run() submits and waits, so it returns the finished job in one call.

terminal
curl -X POST https://api.rendobar.com/jobs \
-H "Authorization: Bearer $RENDOBAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "ffmpeg",
"inputs": { "source": "storage://media/raw/interview.mov" },
"params": { "command": "ffmpeg -i source -c:v libx264 -preset slow -crf 20 -c:a aac -movflags +faststart out.mp4" },
"destinations": ["storage://media/exports"]
}'

Returns immediately with a job id. Poll GET /jobs/{id} or register a webhook rather than blocking on the request.

The reference is checked against your account when you submit, so a typo or a connection you don’t own fails immediately instead of after the job has waited its turn. When the job starts, it gets a signed link to that one object. No key leaves your side, and the link can’t be pointed at anything else in the bucket.

That link has to survive the wait before a job starts as well as the run itself, so it is minted to last as long as the longest job any plan allows. That matters more than it sounds. A presigned URL dies with the credentials that signed it, and a URL signed with an assumed role stops working when the role session ends, 1 hour by default, whatever expiry it requested. Why S3 presigned URLs expire early walks through that trap with numbers.

If the bucket holds footage you never want written to, connect it as read only. A read only connection is admitted as a job source and refused as a destination, so no job can deliver into it by mistake, and no default destination can quietly point at it.

Writing the result back

The destinations line above delivers the encode to exports/ in the same bucket after the job completes, under the connection’s file name pattern. The finished file also stays downloadable from the job, so a delivery that fails does not cost you the encode. Delivery gets its own webhook per bucket. Save FFmpeg output to an S3 bucket covers the three ways to get output into S3, including this one, and what each costs.

When Lambda is still the right call

Plenty of video work fits comfortably inside 900 seconds. Probing a file, grabbing a thumbnail or trimming a short clip finishes in seconds, and if your pipeline already lives in AWS there is no reason to move those. Generating a thumbnail when a file lands in S3 is one of them, and even there the function only submits work rather than doing it.

The line is the long tail. User uploads don’t come in a predictable size, and a pipeline that handles 99 files and times out on the hundredth fails the user who uploaded the longest video. A limit measured in hours instead of minutes turns that tail into ordinary jobs.

Where this stops

The long run was paced with -re, so it proves a job may run for 1,002 seconds, not how fast any particular encode goes. For real encode times across presets and resolutions, how long video transcoding takes has the measurements.

It read a public URL. A storage input changes where the bytes come from, not how long the job may run.

The cost is one sample. Jobs bill by compute time, so a run of the same length costs about the same, and a CPU-heavy encode of a given length is still billed for its length.

Lambda’s limits are the documented defaults. Lambda Managed Instances raise the timeout to 90 minutes for asynchronous invocations, and container services such as ECS and Batch have no 900-second limit at all, in exchange for running and scaling them yourself.

Connecting the bucket the job reads from is covered in the Amazon S3 storage guide, including the role setup that needs no access key.

Frequently asked questions

Can AWS Lambda process a video that takes longer than 15 minutes?

Not in a standard invocation. The function timeout tops out at 900 seconds. AWS documents a 90-minute limit only for Lambda Managed Instances invoked asynchronously or through an event source mapping. Anything longer needs a different service, such as a container task or a job API.

How do I process a video in S3 without downloading it to my own server?

Give the processing a reference to the object instead of the bytes. With a presigned URL FFmpeg reads the object over HTTPS. With a storage input the job reads a storage:// reference to the connected bucket and object key, and your server never touches the file.

How big can Lambda's /tmp directory be?

Between 512 MB and 10,240 MB, set per function. A write-then-process pipeline needs room for the input and the output at the same time, so the largest file it can handle is well under that.

Why does my presigned input URL expire before the job finishes?

A presigned URL can't outlive the credentials that signed it. A URL signed with an assumed role stops working when the role session ends, which by default is 1 hour, whatever expiry the URL asked for.

Can a job read from a bucket without being able to write to it?

Yes. A read only connection is admitted as a job source and refused as a destination, so a job can take footage from an archive bucket without any path to overwrite it.

Sources

Tags #ffmpeg#s3#aws-lambda#storage#serverless
All posts
Share
  1. Video APIs that deliver output to your own bucket Guides for the video API
  2. Give a service S3 access without an access key Guides for the video API
  3. Add video processing to n8n Cloud Guides for the video API