Cloudflare Workers + reelsdk
Render faceless shorts from a Worker and keep them in R2
Cloudflare Workers can't start an ffmpeg process, and every isolate is capped at 128 MB of memory with 10 ms of CPU per request on Free and up to 5 min on Paid. reelsdk renders on its own machines: your Worker sends one fetch to start a faceless short, $0.25 up to 60 s, and about 100 s later a signed webhook hands it the mp4 to stream straight into R2.
- Waiting on a fetch costs no CPU time
- The mp4 streams into R2 without sitting in memory
- Web Crypto checks the webhook signature, no dependencies
Error [ERR_METHOD_NOT_IMPLEMENTED]: The child_process.spawn method is not implemented
Error 1102: Worker exceeded resource limits
Sources: workerd child_process source, Cloudflare Error 1102
The same job as one call to reelsdk
const REELSDK = "https://api.reelsdk.com";
const res = await fetch(`${REELSDK}/v1/shorts?wait=0`, {
method: "POST",
headers: {
Authorization: `Bearer ${env.REELSDK_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
topic: "why octopuses have three hearts",
webhook_url: `${url.origin}/reelsdk-webhook`,
}),
});
const { job } = await res.json();
Why ffmpeg breaks here
128 MB, a CPU budget and no processes
Workers run in V8 isolates, not containers, so there is no process to start. With nodejs_compat, node:child_process is only a stub: from the 2026-03-17 compatibility date spawn throws The child_process.spawn method is not implemented, and older dates get [unenv] child_process.spawn is not implemented yet!. Each isolate has 128 MB of memory and 10 ms of CPU per request on Free, 5 min on Paid. Decoding video in memory runs into one of them and ends in Error 1102: Worker exceeded resource limits.
ffmpeg compiled to WebAssembly doesn't get around that: it shares the same 128 MB, spends the same CPU budget and still has to fit the Worker size limit. What Workers do well is wait. Time spent on a fetch isn't CPU time, and an HTTP-triggered Worker keeps running as long as the client stays connected. So the Worker starts the render, reelsdk does the encode, and R2 keeps the result.
Cloudflare Workers next to reelsdk
| Limit | Cloudflare Workers | reelsdk |
|---|---|---|
| Memory | 128 MB per isolate on every plan | Renders on reelsdk machines, inputs up to 500 MB each |
| CPU time | 10 ms on Free, up to 5 min on Paid | Up to 10 min of run time per job |
| Processes | None, child_process is a stub | ffmpeg 8.1, raw arguments through POST /v1/ffmpeg |
| Request body | 100 MB on the Free and Pro plans | POST /v1/uploads up to 100 MB, or pass URLs |
| Long jobs | No duration limit while the client stays connected | ?wait=0 plus a signed webhook, 3 delivery attempts |
Cloudflare Workers limits from Cloudflare Workers limits, checked October 2026.
Set it up
One Worker, two routes, one R2 bucket
You need a reelsdk key, its webhook secret and an R2 bucket. One route starts a short and returns at once, the other receives the signed webhook and streams the mp4 into R2.
Create the bucket and store the secrets
Create a key in the reelsdk dashboard. The
r2_bucketsbinding gives the Workerenv.VIDEOS, andwrangler secret putasks for each value and stores it encrypted, so the Worker reads both fromenv.wrangler.jsonc { "name": "shorts", "main": "src/index.ts", "compatibility_date": "2026-10-01", "r2_buckets": [{ "binding": "VIDEOS", "bucket_name": "shorts" }] }terminal npx wrangler r2 bucket create shorts npx wrangler secret put REELSDK_KEY npx wrangler secret put REELSDK_WEBHOOK_SECRET npx wrangler deployStart the short and receive it
POST /shortscallsPOST /v1/shorts?wait=0and passes reelsdk's 202 straight back, so the Worker spends almost no CPU.POST /reelsdk-webhookchecksReel-Signaturewith Web Crypto, rejects timestamps more than 300 s old, then pipes the mp4 into R2 with the short's title as metadata. A failed download answers 502, so reelsdk tries again, up to 3 attempts. Put/shortsbehind your own auth before you ship, since every call spends your reelsdk credit.src/index.ts interface Env { REELSDK_KEY: string; REELSDK_WEBHOOK_SECRET: string; VIDEOS: R2Bucket; } const REELSDK = "https://api.reelsdk.com"; export default { async fetch(request, env): Promise<Response> { const url = new URL(request.url); if (request.method !== "POST") return new Response("method not allowed", { status: 405 }); if (url.pathname === "/shorts") { const { topic } = await request.json<{ topic: string }>(); const res = await fetch(`${REELSDK}/v1/shorts?wait=0`, { method: "POST", headers: { Authorization: `Bearer ${env.REELSDK_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ topic, duration_s: 45, style: "cinematic", webhook_url: `${url.origin}/reelsdk-webhook`, }), }); return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" }, }); } if (url.pathname === "/reelsdk-webhook") { const body = await request.text(); const signature = request.headers.get("reel-signature"); if (!(await verifyReelSignature(signature, body, env.REELSDK_WEBHOOK_SECRET))) { return new Response("invalid signature", { status: 401 }); } const job = JSON.parse(body); if (job.status !== "succeeded") return new Response("ok"); // We stream the mp4 straight into R2, the Worker never holds it in memory const video = await fetch(job.outputs.video); if (!video.ok) return new Response("download failed", { status: 502 }); await env.VIDEOS.put(`shorts/${job.id}.mp4`, video.body, { httpMetadata: { contentType: "video/mp4" }, customMetadata: { title: job.result?.title ?? "" }, }); return new Response("ok"); } return new Response("not found", { status: 404 }); }, } satisfies ExportedHandler<Env>; async function verifyReelSignature(header: string | null, body: string, secret: string) { const parts = Object.fromEntries((header ?? "").split(",").map((part) => part.split("="))); const age = Math.abs(Date.now() / 1000 - Number(parts.t)); if (!/^[0-9a-f]{64}$/i.test(parts.v1 ?? "") || !(age <= 300)) return false; const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"], ); const signature = Uint8Array.from(parts.v1.match(/../g), (pair: string) => parseInt(pair, 16)); return crypto.subtle.verify("HMAC", key, signature, encoder.encode(`${parts.t}.${body}`)); }
FAQ
Video on Cloudflare Workers, answered
Can I run ffmpeg or ffmpeg.wasm on Cloudflare Workers?
Not a native binary: Workers have no processes, and child_process.spawn throws a not implemented error. ffmpeg.wasm loads, but it shares the isolate's 128 MB of memory and CPU budget, so any real encode ends in Error 1102: Worker exceeded resource limits. Send the encode to an API and let the Worker wait or return.
Does waiting on a render count against my Worker's CPU limit?
No. Time spent waiting on fetch isn't CPU time, and an HTTP-triggered Worker has no duration limit while the client stays connected. A create call waits up to 60 s by default and up to 300 s with ?wait=N, or pass ?wait=0 and take the result on a webhook, as this Worker does.
How do I run my own ffmpeg command from a Worker?
Send the arguments to POST /v1/ffmpeg with your Bearer key. It runs ffmpeg 8.1 without a shell on a fresh machine that is ready in about 1.9 s at the median. Raw ffmpeg costs $0.01 per compute minute, a job can run for up to 10 minutes, and the first $1 is free.
How do I get big files in, and keep the output in R2?
Request bodies to a Worker stop at 100 MB on the Free and Pro plans, and reelsdk's POST /v1/uploads also takes raw files up to 100 MB. Passing URLs gets you inputs up to 500 MB each and 1 GB per job. reelsdk's download links last 7 days, so stream the finished mp4 into R2 as soon as the webhook arrives.
What else can a Worker render, and how long does it take?
POST /v1/slideshow turns photos into a video with transitions, Ken Burns motion, music, voiceover and captions; a 30 s render takes 40 to 45 s at $0.02 per output minute. POST /v1/captions burns word-by-word captions into a 30 s clip in 30 to 43 s. A faceless short takes about 100 s, $0.25 up to 60 s and $0.35 up to 90 s.
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.
