R RenderComp
remotion lambda video-rendering concurrency worker-pool aws

Remotion Lambda Worker Pool Sizing for Concurrent Jobs

By RenderComp Team Editorial policy

Remotion models a video as a pure function from frame number to rendered output. Because frame 50 and frame 150 share no mutable state, they can be computed on separate machines without coordination. On AWS Lambda, Remotion exploits this by splitting a composition into contiguous frame ranges and dispatching each range to a dedicated Lambda invocation. The collection of those invocations is the worker pool.

The pool’s size is determined entirely by one parameter: framesPerLambda. Memory limits, timeout caps, and invocation rate are all constraints on how small or large you can set it.


The Concurrency Formula

Remotion computes the number of workers as frameCount / framesPerLambda. A 300-frame composition with framesPerLambda: 15 produces exactly 20 concurrent Lambda invocations. Set framesPerLambda: 30 on the same composition and you get 10 workers; set it to 5 and you get 60.

By default, Remotion auto-selects framesPerLambda somewhere between 20 and infinity. That range gives Remotion considerable latitude. For a one-off render, accepting the default is reasonable. For a queue-based system processing many jobs on a fixed schedule, the default hands control to Remotion’s heuristic at exactly the point where predictable concurrency matters most.

Passing an explicit value removes the uncertainty:

import { renderMediaOnLambda } from "@remotion/lambda/client";

await renderMediaOnLambda({
  region: "us-east-1",
  functionName: process.env.REMOTION_FUNCTION_NAME!,
  serveUrl: process.env.REMOTION_SERVE_URL!,
  composition: "MyVideo",
  inputProps: { title: "Episode 1" },
  codec: "h264",
  // 300-frame composition → 20 concurrent workers
  framesPerLambda: 15,
  memorySizeInMb: 2048,
  timeoutInMilliseconds: 120_000,
  outName: "episode-1.mp4",
});

The resulting 20 invocations each render their assigned frame range (0–14, 15–29, 30–44, and so on), then upload their segments to S3. A stitch function assembles the final file.


Choosing framesPerLambda

Two forces pull in opposite directions. Smaller values create more workers and a shorter overall render wall-clock time. But each worker incurs Lambda cold-start overhead and an S3 upload at the end. Larger values reduce that overhead but pile more rendering work onto each invocation.

The hard ceiling is Lambda’s maximum execution time of 15 minutes per invocation. A worker rendering a compute-heavy composition with multiple <Video> layers or complex shader effects could spend several seconds on each frame. If you assign too many frames to one worker, the invocation times out and that segment fails. The per-frame render cost of your specific composition determines how low framesPerLambda must stay.

A small utility that derives the safe upper bound from the 15-minute cap:

// Lambda's hard cap is 15 minutes = 900 seconds.
// Leave headroom for cold start and S3 upload at the end of each invocation.
function maxFramesPerLambda(
  secondsPerFrame: number,
  renderBudgetSeconds = 720 // 12 of the 15 available minutes
): number {
  return Math.floor(renderBudgetSeconds / secondsPerFrame);
}

// Lightweight composition, ~0.5 s per frame:
maxFramesPerLambda(0.5); // → 1440

// Heavy composition with video layers, ~3 s per frame:
maxFramesPerLambda(3);   // → 240

Profile a short test render to measure your composition’s per-frame cost, then treat the result of maxFramesPerLambda as a ceiling. Set framesPerLambda below that ceiling, biasing toward lower values when faster individual renders matter, and toward the ceiling when you want to reduce the total Lambda invocation count across a large job volume.


Memory Per Worker

Each Lambda invocation can use up to 10,240 MB of memory, and that allocation applies at the invocation level, not per frame. A worker configured with memorySizeInMb: 2048 has 2,048 MB available across all the frames in its assigned range.

This matters most for compositions that decode large assets at startup. A composition using <Video> components, extensive image sequences, or useAudioCallback hooks loads that data when the invocation starts and holds it in memory while rendering every frame in the batch. A worker handling framesPerLambda: 100 on such a composition keeps more decoded data resident simultaneously than a worker handling framesPerLambda: 10. If memorySizeInMb is too low, the worker will run out of heap mid-render and the invocation fails.

await renderMediaOnLambda({
  region: "us-east-1",
  functionName: process.env.REMOTION_FUNCTION_NAME!,
  serveUrl: process.env.REMOTION_SERVE_URL!,
  composition: "VideoMontage",
  inputProps: { clips: clipList },
  codec: "h264",
  framesPerLambda: 20,
  // Heavier composition with decoded video assets: allocate more per worker
  memorySizeInMb: 4096,
  timeoutInMilliseconds: 300_000,
  outName: "montage.mp4",
});

Higher memorySizeInMb increases Lambda billing per invocation-second. When framesPerLambda is already small and the worker count is large, that cost multiplier across all workers adds up quickly.


Dispatching Concurrent Jobs

Each execution environment instance of the Lambda function can serve up to 10 requests per second under synchronous invocation. When Remotion’s launcher function fans out to child workers, it issues those invocations in rapid succession. Submitting 10 render jobs simultaneously means 10 launchers each kicking off their own worker pools. A 300-frame job at framesPerLambda: 15 spawns 20 child invocations; 10 such jobs running at once generate 200 child invocations nearly simultaneously.

Lambda handles this by scaling out new execution environment instances, each capable of 10 invocations per second. New instances start cold, so in practice the first wave of invocations from a flush of concurrent jobs arrives faster than Lambda can provision capacity. The result is throttling and retries, which translate into latency at the start of the render.

Staggering job dispatch spreads out the invocation burst without serializing the renders themselves:

import { renderMediaOnLambda } from "@remotion/lambda/client";

interface Job {
  composition: string;
  inputProps: Record<string, unknown>;
  outName: string;
}

async function dispatchQueue(
  jobs: Job[],
  delayBetweenJobsMs: number
): Promise<void> {
  const fn = process.env.REMOTION_FUNCTION_NAME!;
  const url = process.env.REMOTION_SERVE_URL!;

  for (const job of jobs) {
    // Fire without awaiting — all renders proceed concurrently
    renderMediaOnLambda({
      region: "us-east-1",
      functionName: fn,
      serveUrl: url,
      composition: job.composition,
      inputProps: job.inputProps,
      codec: "h264",
      framesPerLambda: 15,
      memorySizeInMb: 2048,
      timeoutInMilliseconds: 120_000,
      outName: job.outName,
    }).catch((err) => console.error("render failed:", job.outName, err));

    // Pause between dispatches to spread out the initial invocation surge
    await new Promise<void>((res) => setTimeout(res, delayBetweenJobsMs));
  }
}

Each renderMediaOnLambda call fires without being awaited: all renders run concurrently, and the await on setTimeout only spaces out the Lambda API calls at dispatch time. The right delayBetweenJobsMs depends on framesPerLambda: a lower value means each job’s launcher produces more child invocations, so the same queue flush generates more total API traffic per second.

If consistent job volume still exhausts the default Lambda concurrency limit after staggering, request a reserved concurrency increase on the function through the AWS Lambda console or the AWS CLI. That configuration change raises the execution environment ceiling without requiring any adjustment to framesPerLambda or the dispatch interval in your application code.

Now available

Get 1,000+ Remotion Templates

Pay once — no subscription. Lifetime updates. TypeScript-first.

View pricing →

Free 50

Get the 50 templates as a ZIP

Enter your email and the ZIP link arrives right away.

Send me the 50-template ZIP. I agree to receive RenderComp template updates and product news, including the paid library (a few emails, one-click unsubscribe). Privacy policy

You do not have to use email. The GitHub repository stays public and needs no signup. Open the repository