billflare.

Video transcription on Cloudflare Containers and Workers AI: a per-hour cost model

Pete Stewart explains why ffmpeg cannot run in a plain Worker, moves it into a scale-to-zero Container, sends audio to Whisper on Workers AI, and works out what each billed component costs.

Products
ContainersWorkers AIR2Workers
Source
Author experience𝕏 article
Author
Pete Stewart (@pj_stew)

Key points

  • The author lists why a Worker was ruled out: ffmpeg.wasm is about 31 MB against a 10 MB bundle cap, Workers block compiling WebAssembly fetched at runtime, and 128 MB isolates with CPU limits cannot handle decoding.
  • He uses a basic instance (1/4 vCPU, 1 GiB, 4 GB disk) with sleepAfter idle shutdown, gives the container no bindings, and hands it presigned R2 URLs. He cites per-second billing and the Workers Paid allowance of 375 vCPU-minutes, 25 GiB-hours and 200 GB-hours a month.
  • Whisper large-v3-turbo at 46.63 Neurons per audio minute comes to about $0.00051 a minute; he estimates 500 hours of video at about $20. The article’s cost tables are not in the tool’s plain text and are not reproduced here.
  • Official docs (rechecked 2026-10-14): Containers bill vCPU at $0.000020 per vCPU-second and memory at $0.0000025 per GiB-second beyond the included usage; Workers AI is $0.011 per 1,000 Neurons with 10,000 free a day; the Whisper model page lists $0.000513 per audio minute.

Original sources

  1. Published

    Original post

    Read on 𝕏

    Pete Stewart explains why ffmpeg cannot run in a plain Worker, moves it into a scale-to-zero Container, sends audio to Whisper on Workers AI, and works out what each billed component costs.

    Pete Stewart

    @pj_stew · Original 𝕏 post

    2000 words on technical struggles and solutions when building video transcription for Achene https://t.co/CGES9Ayqem
    Read originalArchived

    Pete Stewart · @pj_stew

    This archive does not contain the complete text.

    De-risking video transcription on Cloudflare: Containers, Workers AI, and a ~150x cost cut

    While building Achene, there are challenges I've had to resolve, many of which are about keeping the platform affordable for the people using it. One of the main drives to building a teach platform was to make a service that doesn't cost £80+ per month to get started when teaching online. While keeping the costs down, I still want to deliver as good, if not a better service than the top platforms currently. Every lesson video on the platform needs captions. That's not optional polish, it's an accessibility requirement, it's an SEO surface (search engines can't watch a video, but they can crawl a transcript), and it's the seed for features we want later: transcript search and translated captions. The question wasn't whether to build captioning, it was how. The obvious shortcut already existed: our video host, Bunny.net, sells a bolt-on captioning product called Transcribe AI, billed per minute of video. One API call, no infrastructure, done. I would have been happy to do this, as Bunny services are great, and I generally like their service. However, it's priced at $0.10 per minute of video, per language — a 60-minute lesson costs $6.00 Our lowest tier is £5 per month, and allows for 50GB of storage. This would be about 5 hours of 1080p video, so would cost about $30 to transcribe. Admittedly that's a one of payment, so potentially paid off at some point in the future, but if the user decided to leave after the first month, it's a net loss, so I would need to factor it into the cost somehow, which makes the pricing go up significantly! This post is about that decision, the architecture I built to prove it out, the real bugs that turned up along the way, and the actual numbers, not marketing numbers, vendor pricing-page numbers, for what transcription costs depending on who does it. One honesty note up front: what's described here is a spike, a deliberately scoped, throwaway-code proof that the architecture works end-to-end against real Cloudflare, Bunny, and Workers AI infrastructure. It validates the mechanism, the cost model, and the latency. It is not yet the shipped, automatic-captioning-on-every-video production feature (that's a separate, spec'd-but-unbuilt phase). The architecture The pipeline has four Cloudflare primitives doing distinct jobs, plus Bunny (today's video host) at the front: Each stage exists because of a specific constraint the previous option couldn't clear. Worth walking through why, because "just use a Worker" was the first thing we tried to make work, and it doesn't. Why FFmpeg can't run in a plain Worker The instinct is to avoid spinning up a whole container just to run ffmpeg. It doesn't hold up: `ffmpeg.wasm`'s core binary is ~31 MB. Workers cap deployed bundles at 10 MB (gzipped) on the paid plan. Workers explicitly block compiling WebAssembly from bytes fetched at runtime, which is exactly how `ffmpeg.wasm` bootstraps itself. Workers isolates cap at 128 MB of memory with tight CPU-time limits. A real demux/decode/encode pass on even a short video blows through both. So audio extraction needs a real, native ffmpeg binary, running somewhere with normal memory and disk. That's what Cloudflare Containers are for. Cloudflare Containers: a Durable Object with a Docker image attached Cloudflare's `Container` class (from `@cloudflare/containers`) extends `DurableObject`, so it inherits per-instance addressing, lifecycle hooks, and idle shutdown (`sleepAfter`) for free — the same primitives we already use elsewhere in the app for other stateful workloads. The container image here is about as small as it gets: `node:22-alpine` plus `apk add ffmpeg`, running a ~200-line, zero-npm-dependency Node HTTP server (`container/transcoder/server.mjs`). One route, `POST /extract`: Download the source video (or point ffmpeg's demuxer straight at an HLS `.m3u8` URL, same binary handles both). `ffprobe` for duration. `ffmpeg -i <source> -vn -ac 1 -ar 16000 -b:a 64k -f segment -segment_time 300 chunk-%03d.mp3`, strip video, downmix to mono, resample to 16 kHz (Whisper's native rate), and slice into fixed 5-minute chunks. `PUT` each chunk straight to a presigned R2 URL. Delete the temp directory. Container instances persist across requests until they sleep, so nothing is allowed to accumulate on disk between runs. The instance sizing matters for the cost model, so it's worth being explicit about Cloudflare's tiers: basic: a quarter of a vCPU, 1 GiB of memory, 4 GB of disk, is enough to download a 240p MP4 and extract audio from it. There's no benefit to sizing up: this workload is I/O- and ffmpeg-bound, not memory-bound, and disk usage peaks at a few hundred MB (source video + a handful of MP3 chunks) for even an hour-long input. One deliberate design constraint: containers get no environment bindings. No R2, no D1, no AI binding reachable from inside the container, it only has plain network egress. So the container never holds a credential. Everything it needs (the source URL, the destination URLs) is handed to it as short-lived, presigned URLs generated by the Worker beforehand. The container is a pure, stateless compute step; all the trust boundaries stay in the Worker. Billing is scale-to-zero and metered to the second: vCPU: $0.000020 / vCPU-second Memory: $0.0000025 / GiB-second Disk: $0.00000007 / GB-second Egress: $0.025/GB in North America/Europe (1 TB included/month) All under the $5/month Workers Paid plan, which already includes 375 vCPU-minutes, 25 GiB-hours, and 200 GB-hours per month before any of the above rates kick in, an allotment this workload is nowhere near saturating at current volume. Workers AI: Whisper large-v3-turbo Once audio chunks land in R2, a Worker fetches one, base64-encodes it, and calls `env.AI.run('@cf/openai/whisper-large-v3-turbo', { audio, language, initial_prompt })`. This is the actual speech-to-text model, running as a first-party Cloudflare binding — no external API call, no separate vendor contract, billed on the same invoice as the rest of the Workers account. It supports ~99 languages, returns segments with word-level timestamps, a ready-made `vtt` field, and free language detection (`transcription_info.language` + `language_probability`), useful for flagging when a video's audio doesn't match the locale it was uploaded under. Pricing is metered in "Neurons," Cloudflare's internal compute-unit for Workers AI: $0.011 per 1,000 Neurons, with 10,000 Neurons/day free on every account, paid or not. `whisper-large-v3-turbo` costs 46.63 Neurons per audio-minute, which works out to $0.00051 per audio-minute, and the free daily allocation alone covers roughly 3.5 hours of audio before any billing starts. Chunking, and why it's mandatory Whisper's effective receptive field is ~30 seconds regardless of how long the input clip is, and base64-encoding audio for the API call inflates payload size by ~33%. Cloudflare's own reference approach byte-slices raw MP3 into ~1 MB chunks, but byte-slicing an already-encoded file is fragile: it can split mid-frame, and there's no clean way to know each chunk's true offset. Since a container is already in the pipeline doing the ffmpeg work, chunking is done properly there instead: fixed 5-minute segments cut on exact time boundaries (`-segment_time 300`), so chunk _N_'s timestamps just need offsetting by `N × 300` seconds when the transcript is stitched back together. Each chunk lands around 2.4 MB raw (~3.2 MB after base64), comfortably inside every relevant size limit. The Whisper vocabulary problem, and the fix Whisper accepts an `initial_prompt` string that biases decoding toward supplied text, the standard trick for nudging a general-purpose model toward domain jargon. Real-world testing against a Welsh place name ("Nant Gwrtheyrn") showed exactly the limit of that mechanism: the model got it _half_ right ("Gwrth" correct, the "-eyrn" ending dropped to a near-English shape) rather than fully wrong or fully right. `initial_prompt` biases; it doesn't guarantee. Checking Whisper's actual parameter list confirms there's nothing resembling Azure Speech's phrase lists or Amazon Transcribe's `SoundsLike` custom-vocabulary feature, Whisper simply wasn't built with a hard vocabulary-pinning mechanism. So a second, independent correction layer was built that doesn't touch the model at all: a pure function that fuzzy-matches (normalised Levenshtein distance, similarity threshold 0.7) each vocabulary term across the transcript after Whisper has already run, and swaps near-miss spellings for the canonical term, checking an optional phonetic "sounds-like" hint alongside the correct spelling, since a rough phonetic respelling is sometimes closer in edit distance to what Whisper actually output than the correct spelling is. It's deterministic, cheap (pure string comparison, no extra model call), and it's the layer that catches what the model's own bias mechanism misses. Real-world result Against a real 116-second test video, the full pipeline, container download, ffprobe, ffmpeg chunking, R2 upload, Whisper transcription, completed in ~13 seconds end to end. Cost of one hour of video, component by component This is the worked cost model for the full production pipeline (container → R2 → Whisper → stitched captions), not just the spike: That's roughly 150x cheaper than the buy option, per hour of video, before even counting the free daily Neuron allowance. At scale: 500 hours of new video uploaded in a month costs ≈ $20 to transcribe on this pipeline. The same 500 hours through Bunny's bolt-on product would cost $3,000. The gap grows, not shrinks, with volume, because there's no bulk discount steep enough on Bunny's side to close a 150x gap. How Cloudflare Workers AI compares to every other transcription API, not just Bunny Bunny isn't the only alternative, it's just the one we already had a billing relationship with. Widening the comparison to every mainstream speech-to-text API (pricing pulled from each vendor's own pricing page, July 2026, converted to a common $/audio-hour basis for comparison): What this means, and what we gave up to get it As you can see from some of the other services, Bunny was very much on the high side of pricing for this, and I guess that's partly because their video streaming is so cheap, they make up the money in the extra services they offer. Unfortunately at the price they offer it, it wasn't viable for Achene. Otherwise I would have likely just used their service. I could have used one of the cheaper batch services, but that gives you a slower services that has to go elsewhere, so we're still managing the third party service and integration, plus we don't know where the data is and how it's stored. Which brings me to another point I haven't mentioned, and that's the privacy of the data, and the compute used to create and manage it. With Cloudflare, we can run all the compute on renewable energy, keep the data is specific locations if required, and verify the security and integrity of the users data. We could even encrypt it if required. None of which we can do with another service. We do have the overhead of managing the transcription and this service, however, as the whole of Achene is built on this architecture, it's not a huge issue. We can now bundle free transcriptions in any language with all tiers without touching the price, as the cost for that 50GB of 1080p video transcription is now $0.155 rather than $30. What's next This spike proves the idea and costings work, but there are still a number of parts to iron out. As you can see in the demo, the transcription works pretty well, and this is with a complicated bilingual video, which is partly in Welsh (hard for AI translation to get right). In the spike I've added the ability to seed suggested vocab and sounds like versions, which works well, if you go through and add them all. The transcription is surprisingly good without them, and with the vocab and sounds like prompts, it's pretty perfect, but the UX for that isn't ideal, as it usually means slight trial and error with transcribing and retrying after adding more vocab. I'm still not totally sure on how to resolve this completely, but I feel having an area where you could edit the text and potential have find and replace functionality would help. I feel having a global vocab you can build up for your whole course would also be good. So there's a way to go before this feature is production ready, but it's currently promising. #buildinpublic #SaaS
Author experienceAccount-wide usage-based products

A budget alert is a horn, not a brake

@GUYUE_LGY points to Cloudflare’s Budget alerts documentation: alerts send email when spend crosses a threshold but do not pause or cap usage. Before letting AI run unattended, the author checks what actually happens at the line.

Other

Meathill: OpenNext KV ISR cache left unpaid TTL work and ballooned storage.

Meathill reports that an OpenNext Cloudflare KV incremental-cache path for Next.js ISR left a TODO for KV TTL, so versioned page snapshots accumulated. Dashboard billable storage showed 3.59 GB-month over the 1 GB free allowance with ongoing extra charges, and historical zombie data reached about 24 GB before he moved to a new namespace. He does not state a dollar invoice total.

Author experienceWorkers

Free plan FAQ: overage errors, reset time and CPU limits

风易 (@Michael_liyang) answers three common questions: whether going over the free quota silently charges you, when quotas reset, and whether the free plan can be used commercially.