R RenderComp
remotion delayrender data-fetching api tutorial

Remotion delayRender and useDelayRender: Loading Async Data Before Render

Remotion delayRender and useDelayRender: Loading Async Data Before Render

Remotion renders every frame as a snapshot of a React component. That creates a specific problem: if your component needs data from an API, a font that has not finished loading, or a texture that is still decoding, Remotion might capture the frame before the async work is done — producing a blank or half-loaded picture.

The fix is delayRender(). It tells Remotion: pause capturing this frame until I signal that my async work is complete. You call delayRender() to open a “hold,” do your asynchronous work, and call continueRender() to release it. In modern Remotion, the recommended way to do this inside a component is the useDelayRender() hook, which scopes the handle for you and future-proofs your code for browser-based rendering. This guide covers the pattern end to end, including the timeout you must respect and the cases where calculateMetadata is the better tool.


Why You Need It

During a render, Remotion mounts your component at a given frame and takes a screenshot. React does not wait for your fetch() to resolve before that screenshot happens. Without a delay, the frame is captured immediately, and your data-dependent UI has not appeared yet.

delayRender() bridges that gap. It pauses the render for that frame until you explicitly say the frame is ready. Common cases where you need it:

  • Fetching JSON from an API before drawing a chart
  • Loading a custom font via the FontFace API before measuring or displaying text
  • Preparing binary assets — images to decode, or WebGL textures in a @remotion/three scene
  • Any asynchronous initialization your first frame depends on

Full API reference: remotion.dev/docs/delay-render.


Remotion recommends the useDelayRender() hook over calling the global functions directly, because it provides scoped delayRender, continueRender, and cancelRender functions and keeps your code compatible with future browser rendering.

import { useCallback, useEffect, useState } from "react";
import { useDelayRender } from "remotion";

type Stats = { revenue: number; growth: number };

export const Chart: React.FC<{ id: string }> = ({ id }) => {
  const { delayRender, continueRender, cancelRender } = useDelayRender();
  const [data, setData] = useState<Stats | null>(null);

  const fetchData = useCallback(async () => {
    const handle = delayRender(`Fetching stats for ${id}`);
    try {
      const res = await fetch(`https://api.example.com/stats/${id}`);
      const json = (await res.json()) as Stats;
      setData(json);
      continueRender(handle);
    } catch (err) {
      cancelRender(err);
    }
  }, [id, delayRender, continueRender, cancelRender]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  if (!data) return null;
  return <div>Revenue: {data.revenue}</div>;
};

The flow is always the same three steps: open a handle, do the async work, close the handle. Pass a descriptive label to delayRender() — it appears in timeout messages and makes debugging far easier when several delays are active at once.


The Classic Global API

You will still see the global functions in older code and many examples. They live in the remotion package and work the same way — delayRender() returns a handle, and continueRender(handle) releases it:

import { delayRender, continueRender, cancelRender } from "remotion";
import { useEffect, useState } from "react";

export const Legacy: React.FC = () => {
  const [handle] = useState(() => delayRender("Loading data"));
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("https://api.example.com/data")
      .then((r) => r.json())
      .then((json) => {
        setData(json);
        continueRender(handle);
      })
      .catch((e) => cancelRender(e));
  }, [handle]);

  return data ? <div>{JSON.stringify(data)}</div> : null;
};

Both styles are valid, but prefer useDelayRender() for new components so your code is ready for browser rendering.


The 30-Second Timeout

There is one rule you cannot ignore: after calling delayRender(), you must call continueRender() within the timeout — 30 seconds by default — or the render fails with a timeout error. This protects you from a hung render waiting forever on a request that never resolves.

If your data genuinely takes longer than the default, you can raise the limit by passing a timeoutInMilliseconds option to delayRender(), and you can also configure timeouts at the composition and render level. But before increasing the timeout, confirm the delay is legitimate — a slow endpoint, a large asset — rather than a bug where you forgot to call continueRender() on some code path. The most common cause of Remotion timeout errors is a missing continueRender() in an error branch. See remotion.dev/docs/timeout for debugging guidance.

Always call cancelRender(error) in your catch block. It fails the render immediately with a useful message instead of letting it hang until the timeout expires.


When to Use calculateMetadata Instead

delayRender() fetches data inside the component, per frame. Often a cleaner approach is to fetch the data once, before the render starts, using calculateMetadata. This function runs before rendering, can be async, and lets you fetch data, then pass it down as props — and even set the composition’s duration and dimensions from that data.

import { Composition, CalculateMetadataFunction } from "remotion";
import { MyVideo, MyVideoProps } from "./MyVideo";

const calculateMetadata: CalculateMetadataFunction<MyVideoProps> = async ({
  props,
  abortSignal,
}) => {
  const data = await fetch(`https://api.example.com/video/${props.id}`, {
    signal: abortSignal,
  }).then((res) => res.json());

  return {
    durationInFrames: Math.ceil(data.seconds * 30),
    props: { ...props, data },
    width: 1920,
    height: 1080,
  };
};

export const Root: React.FC = () => (
  <Composition
    id="MyVideo"
    component={MyVideo}
    fps={30}
    width={1920}
    height={1080}
    defaultProps={{ id: "abc" }}
    calculateMetadata={calculateMetadata}
  />
);

Use calculateMetadata when the data is needed to define the whole composition (especially its duration) or is shared across all frames. Use delayRender() when a specific component loads its own async resource — a font, a texture, a per-component fetch.


Summary Table

ConcernUse
Fetch data once, before render, to set props/durationcalculateMetadata
Async load inside a component (font, texture, per-component fetch)useDelayRender()
New code, future-proof for browser renderinguseDelayRender() hook
Signal frame is readycontinueRender(handle)
Async work failedcancelRender(error)
Work legitimately exceeds 30sRaise timeoutInMilliseconds

FAQ

Q: What does delayRender do in Remotion? It pauses the render of a frame until an asynchronous task completes. You call delayRender() to open a handle, do your async work, and call continueRender(handle) to let the render proceed. Without it, Remotion may capture the frame before your data or assets are ready.

Q: Should I use delayRender or useDelayRender? Prefer useDelayRender() for new components. It provides scoped delayRender, continueRender, and cancelRender functions and keeps your code compatible with browser rendering. The global functions from the remotion package still work and appear in older examples.

Q: Why does my Remotion render time out? The most common cause is that continueRender() was never called on some code path — often inside an error branch. Ensure every delayRender() handle is released by a matching continueRender(), and call cancelRender(error) in your catch block. If the work legitimately exceeds 30 seconds, raise timeoutInMilliseconds.

Q: How long can a delayRender wait? By default, 30 seconds. If you do not call continueRender() within that window, the render fails with a timeout error. You can increase it by passing timeoutInMilliseconds to delayRender().

Q: When should I use calculateMetadata instead of delayRender? Use calculateMetadata when you need the data before rendering to set the composition’s props, duration, or dimensions, or when the data is shared across all frames. Use delayRender() for async work that happens inside an individual component, like loading a font or a WebGL texture.

Q: Can I have multiple delayRender calls at once? Yes. Each delayRender() returns its own handle, and the render continues only when every handle has been released with continueRender(). Pass a descriptive label to each so timeout messages tell you which one is still open.


Build Faster with RenderComp

Data-driven templates depend on getting async loading right. RenderComp provides production-ready Remotion templates — data visualizations, product showcases, social formats, and more — built to accept inputProps and designed with the correct data-loading patterns already in place. You feed in your data; the composition is ready to render.

Browse the collection at rendercomp.com and start rendering on day one.

Now available

Get 1,000+ Remotion Templates

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

View pricing →