R RenderComp
remotion video-automation lambda rendering

Remotion Lambda Video Throughput by Workflow Type

By RenderComp Team Editorial policy

Remotion treats a video as a pure function: given frame n, a composition renders exactly the same output every time. Lambda rendering exploits that determinism by slicing a composition into frame ranges and dispatching each range to a separate AWS Lambda invocation. The number of invocations running simultaneously, not the speed of any single one, is the primary lever on how much a pipeline produces.

AWS regions default to 1000 concurrent Lambda functions. A single Remotion render claims at most 200 of those functions, giving a floor of five parallel renders when each one hits the concurrency cap. For typical compositions between 0 and 10 minutes at 30 fps, actual function counts interpolate between 75 and 150, which raises the simultaneous-render ceiling to somewhere between 6 and 13. Serial, parallel-batch, and queue-driven workflows each claim a different fraction of that capacity, and the gap compounds over a week of continuous operation.


How Lambda Slices a Composition

Before a render starts, Remotion calculates how many Lambda functions to spawn. For Full HD videos between 0 and 10 minutes long at 30 fps, the default interpolates the function count from 75 at the short end to 150 at the 10-minute mark. A 5-minute composition at 30 fps totals 9,000 frames; with 112 functions (the interpolated midpoint for that duration), each Lambda receives roughly 80 frames to render.

Frame distribution has a hard floor of 20 frames per invocation. For a 300-frame clip with a concurrency target near 75, the floor means Remotion spawns only 15 functions (300 ÷ 20 = 15) rather than 75, because no invocation can render fewer than 20 frames. The 200-function cap acts as the upper bound above the interpolated range. Beyond those defaults, each Lambda invocation runs for at most 15 minutes and can consume up to 10,240 MB of memory. Remotion’s default rendering setup targets Full HD videos shorter than about 80 minutes. Output files cap at approximately 5 GB, corresponding to roughly 2 hours of Full HD video.


Serial Rendering

The simplest pipeline fires one render and polls for completion before starting the next. At any given moment, that render occupies at most 200 of the 1,000 regional Lambda functions — 20% utilization.

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

const REGION = "us-east-1" as const;
const FUNCTION_NAME = process.env.REMOTION_FUNCTION_NAME!;
const SERVE_URL = process.env.REMOTION_SERVE_URL!;

async function renderSerial(
  jobs: Array<{ composition: string; inputProps: Record<string, unknown> }>
) {
  const outputs: string[] = [];

  for (const job of jobs) {
    const { renderId, bucketName } = await renderMediaOnLambda({
      region: REGION,
      functionName: FUNCTION_NAME,
      serveUrl: SERVE_URL,
      composition: job.composition,
      codec: "h264",
      inputProps: job.inputProps,
    });

    let progress = await getRenderProgress({
      renderId,
      bucketName,
      functionName: FUNCTION_NAME,
      region: REGION,
    });

    while (!progress.done) {
      await new Promise((r) => setTimeout(r, 3_000));
      progress = await getRenderProgress({
        renderId,
        bucketName,
        functionName: FUNCTION_NAME,
        region: REGION,
      });
    }

    if (progress.outputFile) {
      outputs.push(progress.outputFile);
    }
  }

  return outputs;
}

Each render’s internal Lambda functions work in parallel, but no second render is in flight while the first completes. If each render takes T seconds, 100 videos take 100 × T seconds. The remaining 800 regional Lambda slots sit idle throughout.


Parallel Batch Rendering

Firing multiple renders simultaneously distributes jobs across all available parallel-render slots. For 10-minute Full HD compositions, each render uses about 150 functions, which means six renders fit inside the 1,000-function regional cap (1,000 ÷ 150 ≈ 6). For 5-minute compositions at 112 functions each, five renders consume 560 functions and leave headroom for a sixth.

async function renderParallelBatch(
  jobs: Array<{ composition: string; inputProps: Record<string, unknown> }>,
  maxConcurrent = 5 // adjust based on your composition's function count
) {
  const pending = [...jobs];
  const outputs: string[] = [];

  while (pending.length > 0) {
    const slice = pending.splice(0, maxConcurrent);

    const results = await Promise.all(
      slice.map(async (job) => {
        const { renderId, bucketName } = await renderMediaOnLambda({
          region: REGION,
          functionName: FUNCTION_NAME,
          serveUrl: SERVE_URL,
          composition: job.composition,
          codec: "h264",
          inputProps: job.inputProps,
        });

        let progress = await getRenderProgress({
          renderId,
          bucketName,
          functionName: FUNCTION_NAME,
          region: REGION,
        });

        while (!progress.done) {
          await new Promise((r) => setTimeout(r, 3_000));
          progress = await getRenderProgress({
            renderId,
            bucketName,
            functionName: FUNCTION_NAME,
            region: REGION,
          });
        }

        return progress.outputFile ?? null;
      })
    );

    outputs.push(...results.filter((o): o is string => Boolean(o)));
  }

  return outputs;
}

The maxConcurrent default of 5 is conservative, sized for compositions that approach the 200-function cap. For shorter videos where the interpolated function count is closer to 75, you can safely raise it. A batch of 30-second clips at 30 fps totals 900 frames; with a concurrency target near 75, each render actually uses only 15 functions due to the 20-frame floor. At 15 functions per render, 13 renders fit inside the 1,000-function cap. Adjust maxConcurrent to the composition’s actual function count rather than treating 5 as a universal constant.


Queue-Driven Dispatch

Parallel batching has a straggler problem: Promise.all waits for the slowest render in each slice before starting the next batch. A saturated queue launches the next render the moment any slot clears, keeping utilization close to the regional ceiling continuously.

import PQueue from "p-queue";

async function renderWithQueue(
  jobs: Array<{ composition: string; inputProps: Record<string, unknown> }>,
  concurrency = 5
) {
  const queue = new PQueue({ concurrency });
  const outputs: string[] = [];

  for (const job of jobs) {
    queue.add(async () => {
      const { renderId, bucketName } = await renderMediaOnLambda({
        region: REGION,
        functionName: FUNCTION_NAME,
        serveUrl: SERVE_URL,
        composition: job.composition,
        codec: "h264",
        inputProps: job.inputProps,
      });

      let progress = await getRenderProgress({
        renderId,
        bucketName,
        functionName: FUNCTION_NAME,
        region: REGION,
      });

      while (!progress.done) {
        await new Promise((r) => setTimeout(r, 3_000));
        progress = await getRenderProgress({
          renderId,
          bucketName,
          functionName: FUNCTION_NAME,
          region: REGION,
        });
      }

      if (progress.outputFile) {
        outputs.push(progress.outputFile);
      }
    });
  }

  await queue.onIdle();
  return outputs;
}

PQueue maintains exactly concurrency renders in flight at all times. When one finishes, the next job begins immediately rather than waiting for a full slice to drain. Over a week of continuous operation, that difference in scheduling tightness adds up faster than any single-render optimization would.


Short Compositions and the 20-Frame Floor

The 20-frame minimum changes the parallel-render calculus for social media clips. A 10-second clip at 30 fps is 300 frames. With an interpolated concurrency target near 75, Remotion would aim for 4 frames per function. Since 4 is below the floor, the actual invocation count is 300 ÷ 20 = 15 functions per render, not 75.

Fifteen functions per render means 66 renders fit inside the 1,000-function regional cap simultaneously. That shifts the appropriate maxConcurrent value from the 13 implied by 75 functions per render to something closer to 60. Lambda quota utilization at that setting reaches 900 of 1,000 functions (60 renders × 15 functions each), compared to the 200 of 1,000 that serial rendering sustains.

When a pipeline handles a mix of clip lengths, getRenderProgress reports the actual in-flight function count per render. Reading that value at runtime and adjusting the queue concurrency dynamically is more reliable than calculating from assumed frame counts in advance.


Remotion documents the full parameter surface for renderMediaOnLambda, including the framesPerLambda override that bypasses interpolated defaults, across more than 1,000 pages of reference material. Setting framesPerLambda explicitly is the most direct way to fix the function count per render and make maxConcurrent calculations predictable.

Now available

Get 1,000+ Remotion Templates

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

View pricing →