AWS Lambda + FFmpeg layer for serverless video transcoding. Reduces transcoding time from hours to minutes through parallel Lambda invocations — one Lambda per output resolution, all running concurrently.
Real benchmark: a 30-second source video transcoded to 4 resolutions (1080p, 720p, 480p, 360p) completes in ~2 minutes total using parallel Lambdas. The same job on a traditional server takes 10–30 minutes due to sequential processing and CPU saturation.
Client / Your Server
│
│ POST { inputUrl, resolutions: ["1080p","720p","480p","360p"] }
▼
┌─────────────────────┐
│ Orchestrator Lambda │ ← single entry point
│ (orchestrator.js) │
└──────────┬──────────┘
│ Invokes all 4 in parallel (Promise.all)
┌───────┼───────┬──────────┐
▼ ▼ ▼ ▼
[1080p] [720p] [480p] [360p] ← each is a separate Lambda execution
│ │ │ │ each has its own CPU + memory
└───────┴───────┴──────────┘
│
▼
Amazon S3
(outputs uploaded)
Each transcoder Lambda (transcoder.js):
- Downloads the source video (HTTPS or
s3://) - Runs the FFmpeg command you supply (with
{input}/{output}placeholders) - Uploads the result to S3
- Returns the S3 key and URL
The orchestrator Lambda (orchestrator.js) fans out all resolutions in parallel using Promise.all, waits for all to finish, and returns a summary with total wall-clock time.
On a traditional server (EC2, VPS, bare metal), video transcoding is one of the most CPU-intensive operations possible. libx264 encoding pegs CPU cores at 100% utilization for the entire duration.
Traditional Server — Sequential Processing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CPU: [████████████ 1080p ████████████][████ 720p ████][██ 480p ██][█ 360p █]
←─── 8 min ──────────────────→←── 5 min ──→←─3 min─→←2 min→
Total: ~18 minutes
Problem 1: Sequential jobs. While transcoding 1080p, all other resolutions wait.
Problem 2: CPU saturated at 100% during each job — server can't serve other requests.
Problem 3: If 3 videos arrive simultaneously, wait time TRIPLES (54 min for 3rd video).
Problem 4: You pay for the server 24/7 even when it's idle.
The real damage happens under load:
| Concurrent upload requests | Wait time for last video (4 resolutions) |
|---|---|
| 1 video | ~18 min |
| 2 videos | ~36 min |
| 3 videos | ~54 min |
| 5 videos | ~90 min |
Each additional concurrent transcode doubles or triples the wait for everyone else. You either:
- Under-provision → users wait hours
- Over-provision → you pay for idle CPU 95% of the time
Lambda — Parallel Processing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lambda 1 (1080p): [████████████ 8 min ████████████]
Lambda 2 (720p): [████ 5 min ████]
Lambda 3 (480p): [██ 3 min ██]
Lambda 4 (360p): [█ 2 min █]
←──── all running simultaneously ────→
Total wall-clock time: ~8 min (longest job)
With 3x concurrent videos: still ~8 min (12 independent Lambdas run in parallel)
Key insight: Lambda gives each invocation its own isolated CPU. 4 resolutions = 4 separate CPUs running simultaneously. Cost scales with actual compute used, not provisioned capacity.
Use the public FFmpeg layer for your region (no build required):
| Region | Layer ARN |
|---|---|
| us-east-1 | arn:aws:lambda:us-east-1:145553500800:layer:ffmpeg:1 |
| eu-west-1 | arn:aws:lambda:eu-west-1:145553500800:layer:ffmpeg:1 |
Or build your own: https://github.com/nicholasess/aws-lambda-ffmpeg-layer
The layer places the ffmpeg binary at /opt/bin/ffmpeg.
npm install
zip -r function.zip src/ package.json node_modules/
aws lambda create-function \
--function-name ffmpeg-transcoder \
--runtime nodejs20.x \
--role arn:aws:iam::YOUR_ACCOUNT:role/lambda-transcoder-role \
--handler src/transcoder.handler \
--zip-file fileb://function.zip \
--timeout 900 \
--memory-size 3008 \
--layers arn:aws:lambda:us-east-1:145553500800:layer:ffmpeg:1aws lambda create-function \
--function-name ffmpeg-orchestrator \
--runtime nodejs20.x \
--role arn:aws:iam::YOUR_ACCOUNT:role/lambda-transcoder-role \
--handler src/orchestrator.handler \
--zip-file fileb://function.zip \
--timeout 900 \
--memory-size 512 \
--environment Variables="{TRANSCODER_FUNCTION_NAME=ffmpeg-transcoder}"aws lambda create-function-url-config \
--function-name ffmpeg-orchestrator \
--auth-type NONE
# Response includes:
# "FunctionUrl": "https://abcdefg.lambda-url.us-east-1.on.aws/"{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject"],
"Resource": "arn:aws:s3:::YOUR-BUCKET/*"
},
{
"Effect": "Allow",
"Action": ["lambda:InvokeFunction"],
"Resource": "arn:aws:lambda:*:*:function:ffmpeg-transcoder"
}
]
}curl -X POST https://YOUR-TRANSCODER-URL.lambda-url.us-east-1.on.aws/ \
-H "Content-Type: application/json" \
-d '{
"command": "-i {input} -vf scale=1280:720 -b:v 2500k -b:a 128k -c:v libx264 -c:a aac -movflags faststart -y {output}",
"inputUrl": "https://example.com/raw-video.mp4",
"outputBucket": "my-transcoded-videos",
"outputKey": "videos/abc123/720p.mp4",
"outputFormat": "mp4"
}'Response:
{
"success": true,
"outputBucket": "my-transcoded-videos",
"outputKey": "videos/abc123/720p.mp4",
"url": "https://my-transcoded-videos.s3.amazonaws.com/videos/abc123/720p.mp4",
"transcodingTime": "118.4s"
}curl -X POST https://YOUR-ORCHESTRATOR-URL.lambda-url.us-east-1.on.aws/ \
-H "Content-Type: application/json" \
-d '{
"inputUrl": "https://example.com/raw-video.mp4",
"outputBucket": "my-transcoded-videos",
"outputPrefix": "videos/abc123",
"resolutions": ["1080p", "720p", "480p", "360p"],
"outputFormat": "mp4"
}'Response:
{
"success": true,
"totalTime": "127.3s",
"succeeded": 4,
"failed": 0,
"outputs": [
{ "resolution": "1080p", "outputKey": "videos/abc123/1080p.mp4", "url": "https://..." },
{ "resolution": "720p", "outputKey": "videos/abc123/720p.mp4", "url": "https://..." },
{ "resolution": "480p", "outputKey": "videos/abc123/480p.mp4", "url": "https://..." },
{ "resolution": "360p", "outputKey": "videos/abc123/360p.mp4", "url": "https://..." }
]
}Any valid FFmpeg argument string works — use {input} and {output} as file placeholders:
{
"command": "-i {input} -vf \"scale=1920:1080,fps=30\" -c:v libx264 -preset fast -crf 23 -c:a aac -b:a 192k -movflags faststart -y {output}",
"inputUrl": "s3://my-raw-bucket/uploads/video.mov",
"outputBucket": "my-transcoded-videos",
"outputKey": "videos/abc123/1080p.mp4"
}Extract audio only:
{
"command": "-i {input} -vn -c:a libmp3lame -b:a 192k -y {output}",
"inputUrl": "https://example.com/video.mp4",
"outputBucket": "my-audio-bucket",
"outputKey": "audio/abc123/audio.mp3",
"outputFormat": "mp3"
}Generate animated GIF:
{
"command": "-i {input} -vf \"fps=10,scale=480:-1:flags=lanczos\" -loop 0 -y {output}",
"inputUrl": "https://example.com/clip.mp4",
"outputBucket": "my-gifs-bucket",
"outputKey": "gifs/abc123/preview.gif",
"outputFormat": "gif"
}| Lambda | Handler | Memory | Timeout | Purpose |
|---|---|---|---|---|
ffmpeg-transcoder |
src/transcoder.handler |
3008 MB | 900s | Runs FFmpeg, uploads to S3 |
ffmpeg-orchestrator |
src/orchestrator.handler |
512 MB | 900s | Fans out parallel jobs |
| Preset | Scale | Video Bitrate | Audio Bitrate |
|---|---|---|---|
1080p |
1920×1080 | 4000k | 192k |
720p |
1280×720 | 2500k | 128k |
480p |
854×480 | 1000k | 128k |
360p |
640×360 | 600k | 96k |
240p |
426×240 | 300k | 64k |
MIT