Netlify + reelsdk

Render narrated slideshows from a Netlify Function

Netlify Functions stop after 60 s, a limit you can't raise, and a buffered response tops out at 6 MB, smaller than most rendered mp4 files. reelsdk renders on its own machines and hands back a link: a 30 s 1080x1920 slideshow with voiceover and captions takes 40 to 45 s, so a 50 s wait returns it before Netlify's cutoff. Slideshows cost $0.02 per output minute plus $0.05 per 1,000 characters of narration.

  • A 50 s wait returns most renders before the 60 s cutoff
  • The function returns a link, not a 6 MB body
  • Longer jobs land in Netlify Blobs through a signed webhook
What a Netlify Function returns when the video is in the body

Response payload size (19040285 bytes) exceeded maximum allowed payload size (6291556 bytes).

errorType: Function.ResponseSizeTooLarge

Source: Netlify Support Forums

The same job as one call to reelsdk

const REELSDK = "https://api.reelsdk.com";

const res = await fetch(`${REELSDK}/v1/slideshow?wait=50`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.REELSDK_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    images,
    voiceover: { text: script },
    webhook_url: `${context.site.url}/api/reelsdk-webhook`,
  }),
});
const { job } = await res.json();
200 with job.outputs.video when it finishes inside 50 s, 202 and a webhook when it doesn't

Why ffmpeg breaks here

60 seconds and 6 MB, and neither one moves

Synchronous Netlify Functions stop after 60 s, and that limit isn't configurable. Buffered request and response payloads are capped at 6 MB, streamed responses at 20 MB, and functions get 1024 MB of memory by default. Even if you bundle a static ffmpeg that fits, the render has to finish inside the minute, and returning the mp4 in the response fails with Function.ResponseSizeTooLarge once it passes 6 MB.

Background functions run for up to 15 minutes, which is enough for many encodes. They answer the caller with a 202 right away, so you still need storage for the result and a way to tell the page it's done, and you still maintain the ffmpeg binary in your repo. With reelsdk, the function holds one fetch for up to 50 s and a webhook covers the rest.

Netlify Functions next to reelsdk

Netlify Functions next to reelsdk
LimitNetlify Functionsreelsdk
Run time60 s for synchronous functions, not configurableUp to 10 min per job, ?wait= holds up to 300 s
Response body6 MB buffered, 20 MB streamedA JSON job with an mp4 link kept for 7 days
Memory1024 MB by defaultRenders on reelsdk machines, inputs up to 500 MB each
Background workBackground functions run 15 min and answer 202?wait=0 plus a signed webhook, 3 delivery attempts
ffmpeg binaryYours to bundle and keep under the limitsffmpeg 8.1 on the render machine, raw arguments through POST /v1/ffmpeg

Netlify Functions limits from Netlify Functions configuration, checked October 2026.

Set it up

One function starts the render, one stores the result

You need a reelsdk key and its webhook secret. The first function asks reelsdk to wait up to 50 s, which covers a 30 s slideshow. Anything slower comes back through a signed webhook that a second function writes to Netlify Blobs.

  1. Set the key and the webhook secret

    Create a key in the reelsdk dashboard. Set both with the Netlify CLI or in the Netlify UI, scoped to all scopes or to Functions, and read them from process.env.

    terminal
    netlify env:set REELSDK_KEY rk_live_xxx
    netlify env:set REELSDK_WEBHOOK_SECRET whsec_xxx
    
    netlify deploy --prod
    
  2. Start the render

    The function posts photos and a script to POST /v1/slideshow?wait=50. reelsdk reads the script aloud, times the photos to it, adds Ken Burns motion and burns captions. context.site.url is the site's main address, where the webhook function lives. Put the route behind your own auth before you ship, since every call spends your reelsdk credit.

    netlify/functions/slideshow.mts
    import type { Config, Context } from "@netlify/functions";
    
    const REELSDK = "https://api.reelsdk.com";
    
    export default async (req: Request, context: Context) => {
      const { images, script } = await req.json();
    
      const res = await fetch(`${REELSDK}/v1/slideshow?wait=50`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.REELSDK_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          images,
          size: "9:16",
          motion: "kenburns",
          voiceover: { text: script },
          captions: "bold",
          webhook_url: `${context.site.url}/api/reelsdk-webhook`,
        }),
      });
    
      const { job, error } = await res.json();
      if (!res.ok) return Response.json({ error }, { status: res.status });
    
      return Response.json(
        { id: job.id, status: job.status, video: job.outputs?.video ?? null },
        { status: res.status },
      );
    };
    
    export const config: Config = { path: "/api/slideshow" };
    
  3. Store results from the webhook

    When a render outlasts the wait, the first function returns a 202 and reelsdk POSTs the finished job here. The function reads the raw body, checks Reel-Signature with your whsec_ secret, rejects timestamps more than 300 s old and writes the status and mp4 link to a Netlify Blobs store, keyed by job id, for your page to read.

    netlify/functions/reelsdk-webhook.mts
    import { getStore } from "@netlify/blobs";
    import type { Config } from "@netlify/functions";
    import { createHmac, timingSafeEqual } from "node:crypto";
    
    export default async (req: Request) => {
      const body = await req.text();
      const signature = req.headers.get("reel-signature") ?? "";
    
      if (!verifyReelSignature(signature, body, process.env.REELSDK_WEBHOOK_SECRET!)) {
        return new Response("invalid signature", { status: 401 });
      }
    
      const job = JSON.parse(body);
      await getStore("renders").setJSON(job.id, {
        status: job.status,
        video: job.outputs?.video ?? null,
        error: job.error?.message ?? null,
      });
      return new Response("ok");
    };
    
    export const config: Config = { path: "/api/reelsdk-webhook" };
    
    function verifyReelSignature(header: string, body: string, secret: string) {
      const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
      const age = Math.abs(Date.now() / 1000 - Number(parts.t));
      if (!/^[0-9a-f]{64}$/i.test(parts.v1 ?? "") || !(age <= 300)) return false;
    
      const expected = createHmac("sha256", secret).update(`${parts.t}.${body}`).digest();
      return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
    }
    

FAQ

Video on Netlify Functions, answered

Why does my Netlify Function fail with Function.ResponseSizeTooLarge when returning a video?

Netlify caps buffered function responses at 6 MB, and streamed responses at 20 MB. Most rendered mp4 files are bigger than that, so returning the bytes from the function fails. With reelsdk the function returns a small JSON job with an mp4 download link that stays up for 7 days.

Can I run ffmpeg in a Netlify Background Function instead?

You can. Background functions run for up to 15 minutes with 1024 MB of memory by default, but they answer the caller with a 202 right away, so you need storage and a status check, and you still ship the ffmpeg binary. With reelsdk each job gets its own machine, ready in about 1.9 s at the median, for up to 10 minutes.

How do I pass my own ffmpeg flags from Netlify?

Send the arguments as an array to POST /v1/ffmpeg. reelsdk runs ffmpeg 8.1 directly, without a shell, on inputs up to 500 MB each and 1 GB per job. It costs $0.01 per compute minute with no subscription, and the first $1 is free.

How long do captions and shorts take?

Burning word-by-word captions into a 30 s clip takes 30 to 43 s, transcription included, at $0.02 per output minute. A faceless short from a topic takes about 100 s and costs $0.25 up to 60 s or $0.35 up to 90 s, so start shorts with ?wait=0 and let the webhook deliver them.

What happens when a render takes longer than Netlify's 60 seconds?

With ?wait=50 reelsdk answers before Netlify stops the function. If the video is still rendering at 50 s you get a 202 with the job and a poll URL, the render keeps going, and reelsdk POSTs the finished job to your webhook_url with up to 3 delivery attempts.

All integrations

Ready to build?

Ship video from your app today

Your first $1 of renders is free, and one key covers every endpoint.

Moonlit lake with a lit cabin under the mountains