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
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();
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
| Limit | Netlify Functions | reelsdk |
|---|---|---|
| Run time | 60 s for synchronous functions, not configurable | Up to 10 min per job, ?wait= holds up to 300 s |
| Response body | 6 MB buffered, 20 MB streamed | A JSON job with an mp4 link kept for 7 days |
| Memory | 1024 MB by default | Renders on reelsdk machines, inputs up to 500 MB each |
| Background work | Background functions run 15 min and answer 202 | ?wait=0 plus a signed webhook, 3 delivery attempts |
| ffmpeg binary | Yours to bundle and keep under the limits | ffmpeg 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.
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 --prodStart 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.urlis 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" };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-Signaturewith yourwhsec_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.
The same fix on other platforms
All integrationsReady to build?
Ship video from your app today
Your first $1 of renders is free, and one key covers every endpoint.
