Skip to content

Encoding

David Torcivia edited this page Aug 4, 2026 · 2 revisions

Encoding on Cloudflare

ffmpeg cannot run in a Worker: no subprocesses, 128 MB memory, CPU limits per request. Cloudflare Containers can run it, since they run an arbitrary OCI image, and the existing apps/worker image is one.

Status: not built. This page records what the platform allows and what would have to change.

Instance types

Type vCPU Memory Disk
lite 1/16 256 MiB 2 GB
basic 1/4 1 GiB 4 GB
standard-1 1/2 4 GiB 8 GB
standard-2 1 6 GiB 12 GB
standard-3 2 8 GiB 16 GB
standard-4 4 12 GiB 20 GB

Custom instances allow at most 4 vCPU and 12 GiB, minimum 3 GiB per vCPU. Account limits are 1,500 concurrent vCPU and 30 TB concurrent disk, so many containers can run at once even though each one is small.

Three constraints

20 GB maximum disk. The worker downloads the whole source before decoding, because ffmpeg wants a seekable file and a proxy ladder reads the source repeatedly. A 40 GB camera master does not fit on any instance type. This is the blocker, and it is the same design point that makes object-storage encoding expensive in transfer terms generally.

No GPU. Software encoding only. nyx currently does real VAAPI (vaapi:/dev/dri/renderD128:QVBR:low-power). Software 4K is substantially slower, and the difference is unmeasured.

4 vCPU ceiling per container. Per-job latency is bounded by 4 cores. Throughput scales by running more containers, which the account limits allow.

What would have to change

The 20 GB cap forces the fix already wanted for cost reasons: stop downloading whole sources.

Options, none implemented:

  • Range reads against R2, so ffmpeg reads only the parts it needs. Requires the worker to present the source to ffmpeg as a seekable stream rather than a file.
  • Segmented encoding: split the source by byte or time range, encode segments in separate containers, concatenate. Fits the instance sizes well and parallelises, but is a real change to the job model.
  • Keep whole-file download and accept a source size limit of roughly 15 GB after leaving room for renditions. Simplest, and rules out camera masters, which is most of the intended use.

Cost sketch

Rates from Pricing. A standard-4 costs roughly:

  • vCPU: 4 x $0.000020/s = $0.288/hour at full utilisation, billed on actual use
  • Memory: 12 GiB x $0.0000025/s = $0.108/hour, billed as provisioned
  • Disk: 20 GB x $0.00000007/s = $0.005/hour, billed as provisioned

About $0.40/hour with the CPU busy. A comparable spot VM is a few cents an hour, so this trades roughly an order of magnitude of compute cost for having no machine to operate.

Container egress is not free (NA/EU $0.025/GB after 1 TB), unlike R2 egress. Whether traffic between a container and an R2 bucket counts as billable egress is not confirmed here and should be checked before relying on it.

Cloudflare Stream

Stream is a managed encode and delivery product. It is not a fit for the review pipeline: onelight needs specific artefacts that Stream does not produce, including frame-accurate proxies carrying tmcd timecode, sprite sheets, waveform data, and reference HDR renditions. Playback delivery is already free through R2.

Recommendation

Containers are viable and would make the deployment fully Cloudflare-native, but only after sources stop being downloaded whole. That change is worth making regardless, because it is also the fix for the transfer cost measured in Benchmarks.

Order: measure transfer versus encode first, then decide between range reads and segmented encoding, then run it on Containers.

workers.dev is not a reliable origin

Measured across acceptance runs on 2026-08-03. A freshly deployed *.workers.dev URL does not answer consistently for some minutes, and not in a way that settles:

claim: 401
claim: 401
claim: 401
claim: 404, body "error code: 1042"   (milliseconds later)

Three consecutive correct answers followed immediately by a miss. Individual edge locations appear to learn the route independently, so any single request can fail regardless of how many succeeded before it. Waiting longer reduces the odds; it does not remove them.

This matters beyond CI. A media worker pointed at a deployment makes a long sequence of requests -- claim, fetch source, upload each rendition, report -- and a single miss fails a job. Any client of a workers.dev deployment therefore needs to retry, and a production deployment should sit behind a custom domain rather than the workers.dev subdomain.

Clone this wiki locally