# Process video from an S3 bucket with FFmpeg

Canonical: https://rendobar.com/blog/process-s3-video-ffmpeg/
Author: Abdelrahman Essawy
Published: 2026-09-05
Updated: 2026-09-13

---

## Key takeaways

- Lambda stops an invocation at 900 seconds and gives it between 512 MB and 10,240 MB of /tmp, so a long transcode of a large S3 object runs into limits that have nothing to do with FFmpeg.
- A job has no 900-second wall. One we ran lasted 1,002 seconds from start to finish, 1,000.3 of them inside FFmpeg, and completed normally.
- That run billed $1.02, about a tenth of a cent per second. Downloading its input took 40 ms and uploading the 57.3 MB result took 2.2 seconds.
- A storage input hands the job a signed link to one object, minted to outlive the longest job any plan allows. A job that waits before it starts does not find its source already expired.
- Read only connections accept a bucket as a source and refuse it as a destination, for footage you never want written to.

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, {formatTimeout(PLAN_LIMITS.free.maxJobTimeout)} on Free and
{formatTimeout(PLAN_LIMITS.pro.maxJobTimeout)} on Pro.

_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:

| Limit | Value | What it means for video |
|---|---|---|
| Function timeout | 900 seconds | The encode, the download and the upload all fit inside 15 minutes |
| `/tmp` storage | 512 MB to 10,240 MB | Input and output both have to fit at once |
| Memory | 128 MB to 10,240 MB | CPU is allocated in proportion, one vCPU at 1,769 MB |
| Synchronous payload | 6 MB | The video can't travel in the request or the response |
| Network bandwidth | 625 Mbps per execution environment | Pulling 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:

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

| Step | Time |
|---|---:|
| Waiting to start | 0.8 s |
| Download input | 40 ms |
| FFmpeg | 1,000,274 ms |
| Upload the 57.3 MB output | 2,175 ms |
| **Start to finish** | **1,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.

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

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);`}
  curl={`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"]
    }'`}
/>

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](/blog/s3-presigned-url-expires-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](/blog/ffmpeg-output-to-s3/) 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](/blog/video-thumbnail-s3-upload/) 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](/blog/video-transcoding-time/) 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](/docs/storage/amazon-s3), including the role setup that needs no access key.
