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
What a Worker hits when it reaches for ffmpeg

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();
202 with job.id right away, then a signed webhook with the mp4 about 100 s later

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

Cloudflare Workers next to reelsdk
LimitCloudflare Workersreelsdk
Memory128 MB per isolate on every planRenders on reelsdk machines, inputs up to 500 MB each
CPU time10 ms on Free, up to 5 min on PaidUp to 10 min of run time per job
ProcessesNone, child_process is a stubffmpeg 8.1, raw arguments through POST /v1/ffmpeg
Request body100 MB on the Free and Pro plansPOST /v1/uploads up to 100 MB, or pass URLs
Long jobsNo 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.

  1. Create the bucket and store the secrets

    Create a key in the reelsdk dashboard. The r2_buckets binding gives the Worker env.VIDEOS, and wrangler secret put asks for each value and stores it encrypted, so the Worker reads both from env.

    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 deploy
    
  2. Start the short and receive it

    POST /shorts calls POST /v1/shorts?wait=0 and passes reelsdk's 202 straight back, so the Worker spends almost no CPU. POST /reelsdk-webhook checks Reel-Signature with 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 /shorts behind 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.

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