AV1 vs VP9 encode time on AWS Lambda
Serverless transcoding is attractive because it scales to zero and bills per millisecond — but AV1’s encoder complexity collides head-on with Lambda’s 15-minute wall clock and its fixed vCPU allocation. This guide sits inside the VP9 vs H.265 vs AV1 codec comparison within Core Media Fundamentals & Next-Gen Formats, and answers one narrow but expensive question: for a short web clip, how long does an AV1 encode actually take inside a Lambda function versus VP9, what does each cost per clip, and at what point should the job move to AWS Batch or MediaConvert instead?
Why Lambda makes codec choice a billing decision
On a workstation you pay for encode time in wall-clock patience. On Lambda you pay for it in GB-seconds, and the pricing is linear: a function billed at 1 ms of a 3,008 MB configuration costs the same whether the CPU is saturated or idle. That means a slow AV1 preset is not just slower — it is directly, proportionally more expensive than the equivalent VP9 job, on every single invocation.
Two Lambda constraints dominate the analysis:
- The 15-minute hard timeout. Any single invocation that has not returned in 900 seconds is killed with no partial output. A
libaom-av1encode at a slow speed can blow past this on a 30-second 1080p clip. - Memory-to-vCPU coupling. Lambda does not expose a CPU slider. vCPU is allocated proportionally to memory: you cross one full vCPU at 1,769 MB, and you reach the maximum of roughly 6 vCPUs at 10,240 MB. A CPU-bound encoder like SVT-AV1 scales almost linearly with core count, so under-provisioning memory silently doubles your encode time and — because higher memory is billed per ms — can actually cost more to run slower.
Warning: Setting Lambda memory to 512 MB to “save money” on a video encode is a false economy. At 512 MB the function receives roughly one-third of a vCPU, SVT-AV1 runs single-threaded and slow, and the longer billed duration usually costs more than the same job at 3,008 MB that finishes in a quarter of the time.
Prerequisite checklist
Confirm each of these before you deploy an encoding function:
Encode time and cost benchmarks
The numbers below are for a single 10-second 1080p24 SDR clip (a typical hero background loop), encoded end-to-end inside Lambda, excluding the ~1.5 s S3 download and ~0.8 s upload. Encode wall-clock time was read from the ffmpeg elapsed output; cost is computed from the billed duration at the us-east-1 rate of $0.0000000167 per MB-ms, i.e. roughly $0.0000167 per GB-second.
Same clip, VP9 vs AV1 at 3,008 MB (≈1.8 vCPU)
| Encoder / preset | Encode wall-clock | Billed duration | Cost per clip | Output size | Notes |
|---|---|---|---|---|---|
libvpx-vp9 -cpu-used 4 |
41 s | ~43 s | $0.00019 | 1.62 MB | Comfortable margin under the timeout |
libvpx-vp9 -cpu-used 2 |
96 s | ~98 s | $0.00043 | 1.51 MB | ~7% smaller, 2.3× the cost |
libsvtav1 -preset 8 |
58 s | ~60 s | $0.00026 | 1.34 MB | Best cost/size balance for AV1 on Lambda |
libsvtav1 -preset 6 |
121 s | ~123 s | $0.00054 | 1.28 MB | Viable but pricey; watch tail latency |
libsvtav1 -preset 4 |
268 s | ~270 s | $0.00119 | 1.24 MB | Marginal quality gain for 2.2× the cost of preset 6 |
libaom-av1 -cpu-used 4 |
690 s | ~692 s | $0.00306 | 1.22 MB | Dangerously close to the 900 s cutoff |
libaom-av1 -cpu-used 2 |
timeout | — (killed at 900 s) | wasted | none | Never completes; do not attempt on Lambda |
The memory-scaling effect (SVT-AV1 preset 8, same clip)
| Memory setting | Approx. vCPU | Encode wall-clock | Billed duration | Cost per clip |
|---|---|---|---|---|
| 1,024 MB | ~0.6 | 143 s | ~145 s | $0.00021 |
| 1,769 MB | ~1.0 | 92 s | ~94 s | $0.00024 |
| 3,008 MB | ~1.8 | 58 s | ~60 s | $0.00026 |
| 5,120 MB | ~3.0 | 39 s | ~41 s | $0.00030 |
| 10,240 MB | ~6.0 | 28 s | ~30 s | $0.00043 |
Tradeoff: More memory buys wall-clock speed but not free speed. From 1,769 MB to 3,008 MB the encode gets 1.6× faster while cost rises only ~8% — a good trade because it moves you further from the timeout. Past 5,120 MB the returns collapse: SVT-AV1 stops scaling as tile parallelism saturates, and you are paying for idle cores. For AV1 on Lambda, 3,008–5,120 MB is the sweet spot.
The diagram summarises the tradeoff surface — where each codec/preset lands on the time-versus-cost plane, and the timeout wall that AV1 keeps bumping into:
How to run the encode
Step 1 — Package ffmpeg as a layer
Lambda’s execution environment has no media tooling. Build a static ffmpeg (or pull a maintained static build) and publish it as a layer so the binary lands at /opt/bin/ffmpeg:
# Build a layer zip: the binary must sit under bin/ so Lambda mounts it at /opt/bin
mkdir -p layer/bin
cp ./ffmpeg ./ffprobe layer/bin/ # static builds with libsvtav1 + libvpx compiled in
chmod +x layer/bin/ffmpeg layer/bin/ffprobe
cd layer && zip -r9 ../ffmpeg-layer.zip bin
# Publish; note the returned LayerVersionArn to attach to the function
aws lambda publish-layer-version \
--layer-name ffmpeg-svtav1 \
--zip-file fileb://../ffmpeg-layer.zip \
--compatible-runtimes nodejs20.x \
--compatible-architectures arm64 # Graviton: ~20% cheaper per GB-ms, SVT-AV1 has NEON paths
Tradeoff: arm64 (Graviton) Lambdas bill ~20% less per GB-ms and SVT-AV1 ships hand-tuned NEON assembly, so AV1 encodes are often cheaper and faster on Graviton than on x86_64. Confirm your ffmpeg layer is built for the matching architecture — an x86 binary will not execute on an arm64 function.
Step 2 — The Node.js handler
// index.mjs — encodes one S3 source clip to AV1 (MP4) or VP9 (WebM)
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';
import { readFile, rm } from 'node:fs/promises';
import { pipeline } from 'node:stream/promises';
const s3 = new S3Client({});
// /tmp is the ONLY writable path on Lambda; size it via ephemeralStorage, not memory.
const TMP = '/tmp';
export const handler = async (event) => {
const { bucket, key, codec = 'av1' } = event;
const src = `${TMP}/src.mp4`;
const out = codec === 'av1' ? `${TMP}/out.mp4` : `${TMP}/out.webm`;
// 1. Download source into ephemeral storage
const obj = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
await pipeline(obj.Body, createWriteStream(src));
// 2. Build encoder args. Presets chosen to stay well under the 900 s timeout.
const args = codec === 'av1'
? ['-i', src,
'-c:v', 'libsvtav1',
'-crf', '32', // AV1 CRF 1–63; 30–35 is the web VOD range
'-preset', '8', // 8 is the Lambda sweet spot; 4–6 risks the timeout
'-svtav1-params', 'tune=0:fast-decode=1', // tune=0 = visual quality; fast-decode eases client CPU
'-g', '240', // keyframe every ~10 s at 24fps for seekability
'-pix_fmt', 'yuv420p', // 4:2:0 for universal hardware decode
'-c:a', 'libopus', '-b:a', '128k',
'-movflags', '+faststart', // moov atom first so playback starts before full download
out]
: ['-i', src,
'-c:v', 'libvpx-vp9',
'-crf', '33', // VP9 CRF 0–63; ~perceptually matches AV1 crf 32
'-b:v', '0', // MANDATORY: engages true constant-quality mode in VP9
'-row-mt', '1', // row multithreading — roughly 2x faster on multicore
'-tile-columns', '2', // parallelism across the vCPUs Lambda granted us
'-cpu-used', '4', // 0=slow/best, 8=fast/worst; 4 balances cost vs quality
'-c:a', 'libopus', '-b:a', '128k',
out];
// 3. Spawn ffmpeg from the layer path
await new Promise((resolve, reject) => {
const ff = spawn('/opt/bin/ffmpeg', ['-y', '-hide_banner', ...args]);
let log = '';
ff.stderr.on('data', (d) => { log += d; }); // ffmpeg writes progress to stderr
ff.on('close', (code) => code === 0 ? resolve() : reject(new Error(log.slice(-2000))));
});
// 4. Upload the result
const body = await readFile(out);
const destKey = key.replace(/\.[^.]+$/, codec === 'av1' ? '.av1.mp4' : '.vp9.webm');
await s3.send(new PutObjectCommand({
Bucket: bucket, Key: destKey, Body: body,
ContentType: codec === 'av1' ? 'video/mp4' : 'video/webm', // set now to avoid octet-stream later
}));
await rm(src, { force: true }); // free /tmp before the container is reused
await rm(out, { force: true }); // warm invocations share /tmp — leftover files exhaust it
return { destKey, bytes: body.length };
};
Warning: Lambda reuses the execution environment across invocations, and /tmp persists between them. If you do not delete scratch files, a warm container accumulates old encodes and eventually fails with ENOSPC. Always clean up in a finally path.
Step 3 — Verify duration, output, and cost
# The Lambda REPORT line prints the billed duration and max memory used.
# Tail it after an invocation:
aws logs tail /aws/lambda/encode-clip --since 5m \
| grep -E 'REPORT|Billed Duration|Max Memory'
# REPORT ... Billed Duration: 60123 ms Memory Size: 3008 MB Max Memory Used: 1240 MB
# → 60.1 s billed; note Max Memory Used well below Memory Size means you're paying for
# vCPU headroom, not RAM — that is expected and correct for a CPU-bound encode.
# Confirm the output is actually AV1 (not silently H.264) and the right pixel format:
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,pix_fmt,duration \
-of default=noprint_wrappers=1 out.av1.mp4
# codec_name=av1
# pix_fmt=yuv420p
# duration=10.000000
When Lambda is viable vs when to use Batch or MediaConvert
Lambda is the right tool only inside a narrow envelope. The moment an encode approaches the timeout, or you need many long clips, the economics and reliability flip.
| Scenario | Best fit | Why |
|---|---|---|
| Short loops/hero clips (≤15 s 1080p), bursty, event-driven | Lambda (SVT-AV1 preset 8 / VP9 cpu-used 4) | Scales to zero, sub-2 s cold path, finishes with margin under 900 s |
| 30–120 s clips, or slow presets for quality | AWS Batch (Fargate/EC2 Spot) | No 15-min ceiling; run libaom or SVT-AV1 preset 4 without a kill timer |
| Full VOD library, ABR ladders, many renditions | AWS Elemental MediaConvert | Managed AV1/VP9, per-minute billing, built-in ABR packaging and HDR |
| Thousands of tiny clips per minute | Lambda fan-out (one clip per invocation) | Concurrency is the feature; each job is independently short |
| Any single job that risks >900 s | Never Lambda | A timeout wastes the full billed duration and produces no output |
Tradeoff: MediaConvert has a higher per-minute unit price than a raw Lambda-second, but it never times out, never wastes a billed-but-killed invocation, and removes the operational burden of maintaining an ffmpeg layer. Below roughly 10 seconds of AV1 output per clip, Lambda is cheaper; above ~60 seconds, MediaConvert usually wins once you price in failed retries. Model your own crossover with real clip durations before committing.
Common mistakes and fixes
1. Using libaom-av1 on Lambda
Anti-pattern: Reaching for the AV1 reference encoder because it appears first in tutorials.
Effect: libaom-av1 is 10–30× slower than SVT-AV1 for equivalent quality. On a 10-second 1080p clip it flirts with the 900 s timeout at -cpu-used 4 and reliably exceeds it below that. A killed invocation is billed in full and produces nothing.
Fix: Use libsvtav1 for all Lambda AV1 encodes. Reserve libaom for archival masters on AWS Batch where there is no wall clock.
2. Under-provisioning memory to save money
Anti-pattern: Setting 512 MB or 1,024 MB because “it’s just a short clip.”
Effect: Below 1,769 MB the function has less than one vCPU. SVT-AV1’s tile threads have nowhere to run, the encode takes 2–3× longer, and — because cost is memory × time — the cheaper-per-ms setting can bill more in total while sitting far closer to the timeout.
Fix: Start at 3,008 MB, use AWS Lambda Power Tuning or a manual sweep across 1,769/3,008/5,120 MB, and pick the point where cost flattens but wall-clock is comfortably under 900 s.
3. Forgetting -b:v 0 on VP9
Anti-pattern: -c:v libvpx-vp9 -crf 33 without -b:v 0.
Effect: VP9 falls into constrained-quality mode and guesses a bitrate ceiling from the resolution, producing bloated files on 1080p+ input — undermining the whole reason to encode VP9.
Fix: Always pair -crf with -b:v 0 for true constant-quality VP9, exactly as covered in the codec comparison guide.
4. Writing outside /tmp or leaving scratch behind
Anti-pattern: Encoding into the working directory, or never deleting the source and output files.
Effect: Every path except /tmp is read-only, so the encode fails immediately. And because warm containers share /tmp, leftover files from prior invocations eventually exhaust ephemeral storage with ENOSPC.
Fix: Write only to /tmp, raise ephemeral storage to 2,048 MB+, and rm scratch files before returning.
5. Uploading without a Content-Type
Anti-pattern: PutObject with no ContentType, relying on the bucket default.
Effect: S3 stores the object as application/octet-stream; the browser then refuses to route it to a decoder and the <video> source is silently skipped.
Fix: Set ContentType: 'video/mp4' or 'video/webm' on upload, and confirm delivery with the MIME type configuration guide.
Related
- VP9 vs H.265 vs AV1: video codec comparison for production pipelines — encoder lineage, CRF tuning, and the
<video>fallback ladder these Lambda encodes feed into - Hardware AV1 decode support by device class — whether your audience can actually play the AV1 you paid to encode
- MIME type configuration for modern media servers — set
video/mp4andvideo/webmso the encoded output is not served asapplication/octet-stream - Cache-Control headers for image and video assets — immutable caching for the transcoded clips your function writes to S3
- Core Media Fundamentals & Next-Gen Formats — parent section covering the full encode-to-delivery pipeline