R RenderComp
remotion rendering lambda infrastructure cost-modeling

Remotion Rendering Costs: Per-Minute API vs. Buy-Once Licensing

By RenderComp Team Editorial policy

Remotion’s central premise (that a video is a React component rendered frame by frame) has a concrete engineering implication that most teams don’t confront until they’ve shipped their first production pipeline: every frame render is a unit of compute, and compute costs money at scale.

When you’re experimenting, this doesn’t matter. When you’re rendering thousands of personalized videos per day, the cost model you chose at the start determines whether your margins hold. Two architectures dominate production Remotion deployments. In the first, you pay per render through a cloud rendering API (Remotion Lambda being the native option), where cost accrues per Lambda invocation and billed compute duration. In the second, you purchase rendering infrastructure or a template license once and absorb it as fixed overhead. The math connecting these models is simple in principle and almost always more nuanced in practice. The variables that drive API cost (duration, composition complexity, concurrency, and chunk granularity) are all directly readable from Remotion’s own APIs.

The goal here isn’t to quote specific pricing tiers; those change and your provider isn’t guaranteed to be mine. The goal is to derive the formula from first principles so you can plug in real numbers from your own pipeline and make an architecture decision that holds past the first invoice.


What Remotion Exposes as Cost Variables

Before writing any formula, pull up useVideoConfig(). This hook returns the canonical parameters of the composition being rendered:

import { useVideoConfig } from 'remotion';

// Inside any Remotion component:
const { fps, durationInFrames, width, height } = useVideoConfig();

These four values are the primary cost variables for any rendering architecture.

  • durationInFrames is the total frame count, the unit of work for any renderer. A 30-second video at 30 fps has 900 frames; at 60 fps it has 1,800. Doubling fps doubles rendering work for the same wall-clock duration.
  • The expression durationInFrames / fps gives duration in seconds, the input for any per-second or per-minute billing formula.
  • width × height determines the compositing surface. A 4K composition isn’t four times harder than 1080p: it’s more like eight to twelve times heavier in practice, due to compositing and asset scaling, though the exact multiplier varies by composition content.

From these, you can derive the quantities that billing formulas actually use:

function deriveRenderCostInputs(config: {
  fps: number;
  durationInFrames: number;
  width: number;
  height: number;
}) {
  const durationSeconds = config.durationInFrames / config.fps;
  const durationMinutes = durationSeconds / 60;

  // Normalized to 1080p (1920×1080 = 2,073,600 px) for a rough complexity index.
  const referencePixels = 1920 * 1080;
  const pixelRatio = (config.width * config.height) / referencePixels;

  return {
    durationSeconds,
    durationMinutes,
    pixelRatio,
    totalFrames: config.durationInFrames,
  };
}

None of these numbers are invented; they’re read directly from the composition. This matters because personalized video pipelines vary composition duration per recipient, and a formula that assumes uniform duration will produce wildly inaccurate cost estimates at volume.


The API Rendering Formula

For a cloud rendering API billed per unit of output video time, the base formula is:

total_cost = duration_minutes × rate_per_output_minute

But rate_per_output_minute is not a constant. It’s a function of at least three Remotion-specific variables:

  1. Composition complexity determines how long each frame actually takes to render in the browser.
  2. Memory allocation drives cost directly: Lambda and similar FaaS runtimes bill by memory × execution time, so a 2048 MB allocation billed for 10 seconds costs exactly twice what a 1024 MB allocation for the same wall time would cost.
  3. Chunk count controls how many parallel invocations handle the render, and thus how many cold starts you’re paying for.

Remotion Lambda’s renderMediaOnLambda() exposes the framesPerLambda option directly:

import { renderMediaOnLambda } from '@remotion/lambda';

const result = await renderMediaOnLambda({
  region: 'us-east-1',
  functionName: 'remotion-render-3-3-82-mem2048mb-disk2048mb-120sec',
  composition: 'MyVideo',
  serveUrl: 'https://your-site.vercel.app',
  codec: 'h264',
  framesPerLambda: 20,       // chunk size: each Lambda handles 20 frames
  concurrencyPerLambda: 1,   // browser tabs per Lambda invocation
  memorySizeInMb: 2048,
  timeoutInSeconds: 120,
});

The chunk count for a given composition is:

function chunkCount(durationInFrames: number, framesPerLambda: number): number {
  return Math.ceil(durationInFrames / framesPerLambda);
}

// 300-frame video, framesPerLambda 20 → 15 Lambda invocations
chunkCount(300, 20); // 15

Each invocation carries cold-start overhead on top of actual frame rendering time. Reducing framesPerLambda increases parallelism and cuts wall-clock render latency, but it multiplies cold-start overhead across more invocations, which can increase total billed compute even as perceived speed improves.

The key trade-off is this: parallelism reduces latency but does not reduce total compute consumed, and in practice often increases it.


Reading Real Cost Data from the Progress API

Rather than estimating from theoretical formulas alone, Remotion Lambda’s getRenderProgress() returns actual billing data from Lambda’s metrics:

import { getRenderProgress } from '@remotion/lambda';

const progress = await getRenderProgress({
  renderId: result.renderId,
  bucketName: result.bucketName,
  functionName: 'remotion-render-3-3-82-mem2048mb-disk2048mb-120sec',
  region: 'us-east-1',
});

// Inspect the real Lambda billing data for this render:
console.log(progress.costs);
// {
//   currency: 'USD',
//   disclaimer: '...',
//   estimatedCost: <actual billed amount>,
//   estimatedDisplayCost: '...',
// }

For cost modeling, the most valuable thing you can do is collect these values across a representative sample of renders (ideally one per composition variant, at both the shortest and longest duration your pipeline will encounter) before committing to an architecture. What you’re looking for is how per-frame cost varies across your composition library. A composition that renders static text for 300 frames costs meaningfully less than one running spring()-animated transforms on 40 DOM nodes for the same 300 frames.


The Break-Even Calculation

With the API cost formula in hand, break-even against a fixed buy-once cost is a one-liner:

function breakEvenMinutes(
  fixedCost: number,      // license or amortized infrastructure cost
  ratePerMinute: number,  // API rate per output minute
): number {
  // Below this threshold, API rendering costs less (no fixed overhead).
  // Above it, fixed infrastructure amortizes to near-zero marginal cost.
  return fixedCost / ratePerMinute;
}

The variable most teams underestimate is volume variance. A pipeline that renders 50 minutes of video in a slow month and 5,000 minutes on a peak month has a different optimal architecture than one with steady throughput. Track variance, not just averages:

type MonthlyRenderStats = {
  month: string;
  totalOutputMinutes: number;
};

function analyzeBreakEven(
  history: MonthlyRenderStats[],
  fixedCost: number,
  ratePerMinute: number,
) {
  const threshold = breakEvenMinutes(fixedCost, ratePerMinute);

  return history.map((m) => ({
    month: m.month,
    outputMinutes: m.totalOutputMinutes,
    apiCost: m.totalOutputMinutes * ratePerMinute,
    fixedCostShare: fixedCost,
    cheaperModel: m.totalOutputMinutes < threshold ? 'api' : 'fixed',
  }));
}

Running this over actual production data (even three months of it) reveals whether your volume consistently sits above or below break-even, or straddles it and demands a hybrid approach: buy-once for steady baseline volume, API for burst.


Concurrency’s Double Effect

Remotion Lambda’s concurrencyPerLambda controls how many browser tabs run inside a single Lambda invocation. Increasing it renders multiple frames simultaneously within one invocation, but each tab competes for the same CPU allocation, and Lambda’s billing doesn’t distinguish between a one-tab and four-tab invocation at the same memory tier.

The effect on total cost depends on whether your composition is CPU-bound or I/O-bound:

// CPU-bound: heavy spring() animations, canvas operations, particle systems.
// concurrencyPerLambda > 2 usually increases wall time without cutting billed compute.
const cpuBoundConfig = {
  concurrencyPerLambda: 1,
  framesPerLambda: 10,   // more chunks → more Lambda-level parallelism
};

// I/O-bound: many remote asset fetches, data requests per frame.
// Higher concurrency lets frames overlap while waiting on I/O.
const ioBoundConfig = {
  concurrencyPerLambda: 4,
  framesPerLambda: 40,   // fewer, larger chunks amortize cold starts better
};

framesPerLambda and concurrencyPerLambda have interacting effects. Small framesPerLambda with low concurrencyPerLambda maximizes invocation-level parallelism, but it multiplies cold-start overhead across many invocations. Inverting that prioritizes cold-start amortization at the expense of latency. Neither is universally correct: the right balance depends on whether per-frame render time dominates over invocation startup time for your specific compositions.


A Composition-Level Cost Probe

For development, this utility component makes cost variables visible in Remotion Studio without any external tooling:

import { useVideoConfig, AbsoluteFill } from 'remotion';

const CostProbe: React.FC<{ ratePerOutputMinute?: number }> = ({
  ratePerOutputMinute,
}) => {
  const { fps, durationInFrames, width, height } = useVideoConfig();

  const durationSeconds = durationInFrames / fps;
  const durationMinutes = durationSeconds / 60;

  const lines = [
    `Resolution : ${width}×${height}`,
    `Frame rate : ${fps} fps`,
    `Frames     : ${durationInFrames}`,
    `Duration   : ${durationSeconds.toFixed(2)}s  (${durationMinutes.toFixed(3)} min)`,
    ratePerOutputMinute != null
      ? `Est. cost  : ${(durationMinutes * ratePerOutputMinute).toFixed(5)} per render`
      : null,
  ].filter((l): l is string => l !== null);

  return (
    <AbsoluteFill
      style={{
        fontFamily:
          '-apple-system, "Segoe UI", Roboto, sans-serif',
        fontSize: 13,
        color: '#e2e8f0',
        background: 'rgba(15,23,42,0.72)',
        padding: '10px 14px',
        alignItems: 'flex-start',
        justifyContent: 'flex-start',
        pointerEvents: 'none',
      }}
    >
      <pre style={{ margin: 0, lineHeight: 1.65 }}>{lines.join('\n')}</pre>
    </AbsoluteFill>
  );
};

Mount <CostProbe ratePerOutputMinute={YOUR_RATE} /> inside your composition during development. When compositions are parameterized (different data inputs produce different durations), seeing the cost-per-render in the Studio preview makes it trivial to spot unexpectedly long renders before they multiply across a batch of thousands.

The ratePerOutputMinute prop is intentionally a parameter, not a constant: it should come from your provider’s actual rate, read from an environment variable, so the component reflects real billing math rather than a stale hardcode.


Wrapping Up

The per-minute cost formula for Remotion API rendering isn’t complicated, but it’s easy to misapply because the inputs are more variable than they appear. Duration isn’t fixed when compositions are data-driven. Complexity isn’t uniform across a template library. Concurrency is a dial that trades latency against total compute in ways that depend on whether your frames spend more time on CPU or waiting for I/O.

Practical takeaways:

  • Read cost variables from useVideoConfig() at composition render time. Don’t hardcode duration assumptions into your formula.
  • Collect real per-render cost data from getRenderProgress().costs rather than estimating from theory alone; the gap between theoretical and actual can be significant for complex compositions.
  • Run the break-even calculation against actual monthly volume history before committing to either architecture. Volume variance matters as much as volume average.
  • Tune framesPerLambda and concurrencyPerLambda together. They interact: optimizing one in isolation can worsen the other’s contribution to total billed compute.
  • A composition that looks short in Remotion Studio can still be expensive at volume if its per-frame render time is high. The cost probe makes this visible before it hits production.

Templates like the ones in the RenderComp catalog are built with configurable durationInFrames and resolution, so the cost probe and break-even analysis above integrate directly: the formula is the same regardless of which composition you’re pricing. Measure first, then commit to an architecture.

Now available

Get 1,000+ Remotion Templates

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

View pricing →