Replit + reelsdk
Caption videos from a Replit app without slowing it down
Replit can run ffmpeg once you add it as a Nix system dependency. The catch is where it runs: on the default Autoscale deployment the encode shares your app's instance with web traffic, and the instance stays awake and bills compute units while it renders. reelsdk gives each job its own machine, ready in about 1.9 s, and burns word-by-word captions into a 30 s clip in 30 to 43 s for $0.02 per output minute.
- Each job gets its own machine, ready in 1.87 s at the median
- Transcription, fonts and word timing come with the call
- A signed webhook tells your app when the mp4 is ready
Error: spawn ffmpeg ENOENT
errno: -2, code: 'ENOENT', syscall: 'spawn ffmpeg', path: 'ffmpeg'
Source: Node.js child_process
The same job as one call to reelsdk
const REELSDK = "https://api.reelsdk.com";
const res = await fetch(`${REELSDK}/v1/captions?wait=0`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REELSDK_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ video_url, style: "bold", webhook_url }),
});
const { job } = await res.json();Why ffmpeg breaks here
ffmpeg runs on Replit. It just runs on your app's machine
Replit apps run on Linux, so ffmpeg itself isn't the problem: add it as a Nix system dependency and spawn ffmpeg ENOENT goes away. The problem is the machine. A deployment has a fixed amount of vCPU and RAM, and an encode will take all of it, so every other request waits while the video renders.
On Autoscale, the default deployment type, you pay while requests are being served: compute units for CPU and memory, plus a fee per million requests. In raw compute a render is cheap. What costs you is everything around it: a busy instance while users wait, a transcription model, caption fonts and a TTS service for captions and voiceover, or a Reserved VM sized for your busiest hour.
Rendering on Replit next to reelsdk
| Topic | On your Replit deployment | reelsdk |
|---|---|---|
| Where the encode runs | On your app's instance, next to web traffic | A fresh machine per job, ready in 1.87 s at the median |
| Billing | Autoscale compute units while a request is served | $0.02 per output minute for captions, no subscription |
| Installing ffmpeg | A Nix system dependency | ffmpeg 8.1 already there, raw arguments through POST /v1/ffmpeg |
| Captions | Your own transcription model, fonts and timing | Transcription and word-by-word timing, plus SRT, VTT and ASS |
| Fixed capacity | Reserved VM from $15 a month for a shared 0.5 vCPU and 2 GB | 3 jobs at once per key, 500 a day |
Replit limits from Replit deployment pricing, checked October 2026.
Set it up
Two secrets, one prompt, one raw-body route
You need a reelsdk key, its webhook secret and the public address of your published app. Replit Agent writes the routes from the prompt below, and the snippets show what to expect so you can check its work.
Add the secrets
Create a key in the reelsdk dashboard. Open the Secrets tool in your Replit App and add three with New Secret.
PUBLIC_URLis your published address, for examplehttps://your-app.replit.app, so reelsdk knows where to send the webhook. Secrets reach your code as environment variables and carry over to Autoscale and Reserved VM deployments.Replit secrets - REELSDK_KEY
- rk_live_...
- REELSDK_WEBHOOK_SECRET
- whsec_...
- PUBLIC_URL
- https://your-app.replit.app
Give Replit Agent the prompt
It adds a captions route, a webhook route and a page with a URL field and a player. The prompt tells the Agent not to install ffmpeg or a transcription model.
Prompt for Replit Agent Add a "Caption this video" feature to this Express app using reelsdk (https://reelsdk.com). Don't install ffmpeg or any transcription model. 1. Read REELSDK_KEY, REELSDK_WEBHOOK_SECRET and PUBLIC_URL from Secrets (process.env). 2. Add POST /api/captions. It takes { video_url } and POSTs to https://api.reelsdk.com/v1/captions?wait=0 with the header "Authorization: Bearer REELSDK_KEY" and the JSON { "video_url": video_url, "style": "bold", "webhook_url": PUBLIC_URL + "/webhooks/reelsdk" }. The response is JSON { job } with HTTP 202. Save job.id with status "queued" in the database. 3. Add POST /webhooks/reelsdk using express.raw({ type: "*/*" }) so the body stays raw. Verify the Reel-Signature header (t=<unix seconds>,v1=<hex>): HMAC-SHA256 of "<t>.<raw body>" keyed with REELSDK_WEBHOOK_SECRET, compared with crypto.timingSafeEqual. Return 401 if it fails or t is more than 300 seconds old. Then save job.status and job.outputs.video for job.id. 4. Add a page with a video URL field, a Caption button, and a player that shows the result when the status is "succeeded".Check the captions route
It posts the video URL to
POST /v1/captions?wait=0with yourwebhook_url, so reelsdk answers with a 202 and the job id in well under a second. Your instance is free again while the captions render, 30 to 43 s for a 30 s clip. If reelsdk can't be reached, the route answers with a 502 instead of hanging.index.js const crypto = require("node:crypto"); const express = require("express"); const app = express(); const REELSDK = "https://api.reelsdk.com"; app.post("/api/captions", express.json(), async (req, res) => { try { const response = await fetch(`${REELSDK}/v1/captions?wait=0`, { method: "POST", headers: { Authorization: `Bearer ${process.env.REELSDK_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ video_url: req.body.video_url, style: "bold", webhook_url: `${process.env.PUBLIC_URL}/webhooks/reelsdk`, }), }); const { job, error } = await response.json(); if (!response.ok) return res.status(response.status).json({ error }); res.status(202).json({ id: job.id, status: job.status }); } catch (error) { console.error("reelsdk request failed", error); res.status(502).json({ error: "render service unreachable" }); } });Check the webhook route
The signature is an HMAC-SHA256 of the exact bytes reelsdk sent, so this route uses
express.rawand must not sit behind an app-wideexpress.json(). It checksReel-Signaturewithcrypto.timingSafeEqual, rejects timestamps more than 300 s old, then saves the status and the mp4 link. reelsdk tries up to 3 times if your app doesn't answer with a 2xx.index.js (continued) app.post("/webhooks/reelsdk", express.raw({ type: "*/*" }), (req, res) => { const body = req.body.toString("utf8"); if (!verifyReelSignature(req.get("Reel-Signature"), body, process.env.REELSDK_WEBHOOK_SECRET)) { return res.status(401).send("invalid signature"); } const job = JSON.parse(body); console.log("reelsdk job finished", job.id, job.status, job.outputs.video); // Save job.status and job.outputs.video for job.id in your database here res.send("ok"); }); function verifyReelSignature(header, body, secret) { 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 expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${body}`).digest(); return crypto.timingSafeEqual(expected, Buffer.from(parts.v1, "hex")); } app.listen(3000, "0.0.0.0");
FAQ
Video on Replit, answered
How do I fix Error: spawn ffmpeg ENOENT on Replit?
Node can't find an ffmpeg binary on the PATH. Add ffmpeg as a Nix system dependency (pkgs.ffmpeg in replit.nix) and restart the app. That's enough for small, occasional jobs. For captions or long renders, send the work to an API so it doesn't run on the same instance as your web app.
Is ffmpeg on Replit more expensive than an API?
Not in raw compute. A 60 s encode on 1 vCPU and 2 GB is about 1,320 Autoscale compute units, well under a cent. The cost is capacity and plumbing: the encode competes with your web traffic, and captions need a transcription model, fonts and word timing on top. reelsdk charges $0.02 per output minute for captions with all of that included.
How do I handle long renders without HTTP timeouts?
Pass ?wait=0 and a webhook_url, as the route above does. reelsdk answers with a 202 and the job id right away, then POSTs the finished job to your app with up to 3 delivery attempts. You can also hold the request open with ?wait=N for up to 300 s, or long-poll GET /v1/jobs/{id}?wait=N.
Can I make complete faceless shorts from a Replit app?
Yes. POST /v1/shorts takes a topic, writes the script, generates scene images in one of 8 visual styles, adds a voiceover in English, Spanish, French, Japanese, Chinese or Korean and burns word-by-word captions with music. A short takes about 100 s and costs $0.25 up to 60 s or $0.35 up to 90 s.
What does it cost and how do I get a key?
Sign up with Google and create a key in the dashboard; each key comes with its webhook secret. The first $1 is free with no subscription. Captions and slideshows are $0.02 per output minute and raw ffmpeg is $0.01 per compute minute. Each key runs 3 jobs at once and 500 a day, inputs go up to 500 MB each, and download links stay up for 7 days.
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.
