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-av1 encode 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:

Encode time versus cost tradeoff on AWS Lambda A scatter plot with encode wall-clock time on the horizontal axis and cost per clip on the vertical axis. VP9 cpu-used 4 and SVT-AV1 preset 8 sit low-left as the efficient choices; SVT-AV1 preset 4 and libaom sit far right near the 900-second timeout wall. Encode wall-clock time (seconds) → Cost per clip → 900 s timeout efficient zone VP9 cpu4 AV1 preset8 VP9 cpu2 AV1 preset6 AV1 preset4 libaom cpu4

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.