Lambda vs EC2 for Remotion Rendering: A Cost Framework
By RenderComp Team Editorial policy
Remotion treats video as a pure function: given a frame index, a composition returns a deterministic image. That model is unusually friendly to distributed computing. Because there is no shared mutable state between frames, you can render frame 0 and frame 899 on completely independent machines, stitch the results, and get an identical video to the one you would have produced sequentially. Lambda is the obvious cloud primitive for this: spin up 45 functions in parallel, each responsible for 20 frames, and your 30-second timeline renders in the time it takes the slowest chunk to complete.
What is not obvious is when that convenience starts costing you more than running the same workload on an EC2 instance. The answer depends on three factors most comparisons ignore: the cold-start tax embedded in every Lambda invocation, how framesPerLambda controls the trade-off between parallelism and overhead, and the break-even render volume below which Lambda’s pay-per-millisecond model wins decisively. This article works through the math with real AWS pricing (verify current rates at aws.amazon.com/lambda/pricing; numbers here reflect us-east-1 as of mid-2026) and gives you a TypeScript utility you can drop into your own pipeline.
How Remotion Lambda Slices a Video
When you call renderMediaOnLambda, the orchestrator Lambda splits the composition’s frame range into chunks and fans them out. framesPerLambda is the primary knob:
import { renderMediaOnLambda } from '@remotion/lambda/client';
const { renderId, bucketName } = await renderMediaOnLambda({
region: 'us-east-1',
functionName: 'remotion-render-4-0-272-mem3008mb-disk2048mb-120sec',
serveUrl: process.env.REMOTION_SERVE_URL!,
composition: 'ProductDemo',
inputProps: { productId: 'acme-pro' },
codec: 'h264',
framesPerLambda: 20, // chunk granularity — discussed in depth below
imageFormat: 'jpeg',
jpegQuality: 80,
});
A 30-second composition at 30 fps produces 900 frames. With framesPerLambda: 20, the orchestrator spawns ceil(900 / 20) = 45 render Lambdas plus one stitching Lambda, for 46 total invocations. That ceiling matters: if framesPerLambda does not divide evenly into the total frame count, the last chunk is short, but it still pays its full cold-start cost.
The Cold-Start Tax
Every Lambda invocation in the Remotion pipeline pays an initialization overhead (Node.js runtime init, the Remotion bundle hydration, and Chrome’s headless startup) before a single frame is rendered. On a 3008 MB function in a warm region, this overhead lands between 2 and 5 seconds depending on bundle size and whether the execution environment was recycled. Call it 3 seconds for planning purposes.
At 3008 MB and 3 seconds of cold start:
cold-start cost per invocation
= (3008 MB / 1024 MB per GB) × 3 s × $0.0000166667 per GB-s
≈ $0.000147
Across 46 invocations:
cold-start overhead alone ≈ 46 × $0.000147 ≈ $0.0068
That is approximately two-thirds of the Lambda bill for a simple composition, which means framesPerLambda directly controls how many times you pay this tax.
A Reusable Cost Estimator
Rather than reaching for a spreadsheet, keep the cost model in the same TypeScript codebase as your render pipeline so it can evolve with your infrastructure choices:
interface LambdaCostParams {
memorySizeInMb: number;
coldStartMs: number; // measure this for your bundle
renderMsPerFrame: number; // benchmark on your composition
framesPerLambda: number;
totalFrames: number;
architecture?: 'x86_64' | 'arm64';
}
interface CostBreakdown {
invocations: number;
computeUsd: number;
requestUsd: number;
totalUsd: number;
}
function estimateLambdaCost({
memorySizeInMb,
coldStartMs,
renderMsPerFrame,
framesPerLambda,
totalFrames,
architecture = 'x86_64',
}: LambdaCostParams): CostBreakdown {
// ARM64 (Graviton2) is ~20 % cheaper for the same GB-second
const gbSecondPrice =
architecture === 'arm64' ? 0.0000133334 : 0.0000166667;
// AWS rounds duration up to 1 ms increments
const requestPrice = 0.0000002; // $0.20 per million requests
const invocations = Math.ceil(totalFrames / framesPerLambda) + 1; // +1 stitcher
const renderMsPerLambda = framesPerLambda * renderMsPerFrame;
const durationMsPerLambda = coldStartMs + renderMsPerLambda;
const gbSeconds =
(memorySizeInMb / 1024) * (durationMsPerLambda / 1000) * invocations;
const computeUsd = gbSeconds * gbSecondPrice;
const requestUsd = invocations * requestPrice;
return {
invocations,
computeUsd,
requestUsd,
totalUsd: computeUsd + requestUsd,
};
}
Wire it into your render dispatch to log actual vs estimated cost, and refine coldStartMs and renderMsPerFrame from CloudWatch data over time:
const estimate = estimateLambdaCost({
memorySizeInMb: 3008,
coldStartMs: 3000,
renderMsPerFrame: 200, // 5 fps effective throughput on a moderately complex composition
framesPerLambda: 20,
totalFrames: 900, // 30s × 30fps
architecture: 'arm64',
});
console.log(`Estimated: $${estimate.totalUsd.toFixed(4)} across ${estimate.invocations} invocations`);
Tuning framesPerLambda for Cost vs Latency
The relationship is not linear. Doubling framesPerLambda from 20 to 40 cuts invocation count roughly in half, saving cold-start overhead, but it extends wall-clock latency because each chunk now takes twice as long. The cost-optimal point is where render work per invocation is large relative to cold-start overhead, typically when renderMsPerFrame × framesPerLambda ≥ 3 × coldStartMs. For the numbers above that means at least 45 frames per Lambda, though latency requirements may force you lower.
There is also a timeout floor: the deployed function’s timeoutInSeconds must exceed (coldStartMs + framesPerLambda × renderMsPerFrame) / 1000 with margin. A complex composition rendering at 2 fps with framesPerLambda: 60 needs at least 33 seconds of headroom before cold start, so budget 120 seconds minimum for safety on slow compositions.
EC2 Rendering Without Cold-Start Overhead
On EC2, a long-running Node.js process never pays a cold-start tax. The trade-off is that you pay for the instance whether or not it is rendering. The worker pattern below mirrors what Lambda does internally (splitting the frame range into per-CPU chunks) but keeps all work on the same box:
import { renderMedia, selectComposition } from '@remotion/renderer';
import os from 'os';
import path from 'path';
async function renderParallel(
serveUrl: string,
compositionId: string,
outputDir: string,
): Promise<string[]> {
const composition = await selectComposition({
serveUrl,
id: compositionId,
inputProps: {},
});
const { durationInFrames } = composition;
const workers = os.cpus().length; // e.g. 4 on c5.xlarge
const framesPerWorker = Math.ceil(durationInFrames / workers);
const chunkPaths = await Promise.all(
Array.from({ length: workers }, async (_, i) => {
const startFrame = i * framesPerWorker;
const endFrame = Math.min(startFrame + framesPerWorker - 1, durationInFrames - 1);
const chunkPath = path.join(outputDir, `chunk-${i.toString().padStart(3, '0')}.mp4`);
await renderMedia({
composition,
serveUrl,
codec: 'h264',
outputLocation: chunkPath,
frameRange: [startFrame, endFrame],
// Chromium shares the same process — no additional cold-start per chunk
browserExecutable: '/usr/bin/chromium-browser',
});
return chunkPath;
}),
);
return chunkPaths; // feed to ffmpeg concat or your stitching step
}
The absence of per-chunk cold starts means EC2’s effective render throughput scales more linearly with vCPU count than Lambda’s does with invocation count. A c5.xlarge (4 vCPU) running four concurrent renderMedia calls processes frames at roughly 4× the single-core rate. A c5.2xlarge (8 vCPU) doubles that again.
Lambda’s CPU Ceiling
Lambda allocates CPU proportionally to memory. At 1769 MB you get exactly 1 vCPU; at 3008 MB you get approximately 1.7 vCPU; at the 10240 MB maximum you get roughly 6 vCPU. This ceiling matters for Chromium-heavy compositions. A c5.xlarge gives 4 full vCPU at $0.170/hour; to get comparable CPU from Lambda you would need ~7000 MB per function, dramatically increasing your GB-second cost.
Break-Even Volume
To find where EC2 becomes cheaper, compute the daily cost of each model at a given render volume N (videos per day):
function dailyCostLambda(videosPerDay: number, costPerVideo: number): number {
return videosPerDay * costPerVideo;
}
function dailyCostEc2(
hourlyRate: number, // on-demand or spot
hoursRunning: number, // 24 for always-on, less for scheduled scaling
): number {
return hourlyRate * hoursRunning;
}
// Break-even: Lambda cost per day = EC2 cost per day
// N * costPerVideo = hourlyRate * hoursRunning
// N = (hourlyRate * hoursRunning) / costPerVideo
function breakEvenVolume(
hourlyRate: number,
hoursRunning: number,
costPerVideo: number,
): number {
return (hourlyRate * hoursRunning) / costPerVideo;
}
For a concrete scenario (a 30-second composition at 30 fps, arm64 Lambda at 3008 MB, framesPerLambda: 20, 3-second cold start, 200 ms render per frame), the estimator above yields approximately $0.008 per video. An always-on c6g.xlarge (ARM Graviton2, 4 vCPU, ~$0.136/hour in us-east-1) runs $3.26/day.
break-even = $3.26 / $0.008 ≈ 408 videos per day, or ~17 per hour
Below 17 renders/hour, Lambda is cheaper. Above it, EC2 wins, and that advantage compounds if you use Spot instances, where c6g.xlarge frequently trades at $0.03–$0.05/hour, dropping the break-even to under 60 videos/day.
ARM64 and Spot as Cost Multipliers
Deploying Remotion Lambda on Graviton2 requires one flag at deploy time and costs nothing else:
import { deployFunction } from '@remotion/lambda';
await deployFunction({
region: 'us-east-1',
memorySizeInMb: 3008,
diskSizeInMb: 2048,
timeoutInSeconds: 120,
architecture: 'arm64', // ~20 % compute cost reduction; no code changes needed
});
Remotion’s Lambda runtime ships pre-built for both architectures, so switching requires no other changes. Combine this with the arm64 branch in estimateLambdaCost above to reflect the lower GB-second rate in your monitoring.
For EC2, ARM Graviton instances (c6g, c7g families) deliver the same ~20% discount, and Spot pricing makes the effective hourly rate volatile but consistently lower than on-demand. Spot interruption is the risk that matters if your render takes more than 2 minutes. Mitigate it by checkpointing rendered chunks to S3 and resuming from the last completed chunk on interruption.
High-Resolution Renders and Ephemeral Storage
One Lambda cost that the GB-second model hides is ephemeral storage. Lambda’s /tmp defaults to 512 MB. A 4K composition (3840×2160) produces PNG frames of roughly 20–25 MB each. With framesPerLambda: 20, that is up to 500 MB of frame data before encoding, right at the default limit before accounting for the encoded output chunk. Increase disk allocation at deploy time:
await deployFunction({
region: 'us-east-1',
memorySizeInMb: 3008,
diskSizeInMb: 4096, // reflected in function name and billed at $0.0000000309/GB-s
timeoutInSeconds: 180,
architecture: 'arm64',
});
The ephemeral storage charge ($0.0000000309 per GB-second) is small but nonzero and not included in the basic cost estimator above. At 4096 MB of disk over a 40-second invocation:
(4096 / 1024) × 40 × 0.0000000309 ≈ $0.000005 per invocation
Negligible per video, but worth including in your model if you are rendering at volume.
EC2 avoids this problem entirely: instance store or attached EBS gives you arbitrarily large scratch space at predictable cost.
Wrapping Up
The decision framework reduces to two questions. First: what is your render volume relative to the break-even point for your instance type? Use the estimator in this article with your own composition’s measured renderMsPerFrame (instrument it with a CloudWatch metric or a simple console.time wrapper) rather than guessing. Second: do you need burst capacity that EC2 cannot absorb without pre-warming? Lambda scales to hundreds of concurrent renders instantly; EC2 needs Auto Scaling with a cold-start lag of its own.
For teams running production render pipelines (the kind backing the templates in the RenderComp catalog), a hybrid is often the answer: EC2 Spot instances handle the predictable daily baseline, and Lambda absorbs spikes without you provisioning headroom that sits idle overnight. The estimateLambdaCost function above makes it straightforward to instrument both paths and let CloudWatch tell you which is actually winning.
The numbers shift as your composition complexity evolves. Measure early, model explicitly, and revisit the break-even calculation whenever a new composition class enters the pipeline.
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →