Skip to main content
Cloudflare R2

You can serve video straight from your own R2 bucket. R2 by itself will not do it.

Object storage stores bytes. Playback needs three more things (an encoding ladder, a cacheable hostname, and a way to restrict a stream), and Cloudflare's own documentation rules out the shortcut most people reach for first.

Four questions, answered first

Each answer links to the section that shows the work.

Can you stream video directly from a Cloudflare R2 bucket?
Yes, once the HLS output already exists in the bucket and your own domain sits in front of it. R2 serves the manifest and segments like any other object. It will not create them, so an encoder has to run before upload. The four pieces
Does Cloudflare R2 charge for video bandwidth?
No. Direct internet egress from R2 is 0 USD under Cloudflare's published pricing. You pay 0.015 USD per GB-month of storage and per-million-request operation fees, and neither one grows with how often a video is watched. What it actually costs
Why can a presigned URL not protect HLS playback?
A presigned URL authorizes one object. An HLS session is a manifest plus hundreds of segment objects whose paths sit in the manifest as plain text. Restricting playback means authorizing each request at the edge, usually a short-lived token checked by a Worker. Restricted playback
Is serving video from R2 against Cloudflare's terms?
Not in the agreement we read on 2026-08-27. The Self-Serve Subscription Agreement effective 2025-09-12 runs sections 2.1 through 2.7, and the non-HTML-content clause people still quote is not among them. Section 2.7, Acceptable Use, still applies. What the terms say

Reviewed 2026-08-27 by the NULX engineering team. Every price below links to its primary source.

The boundary

What R2 does, and what it will never do for you

Most failed attempts start by assuming the bucket is a video service. It is a bucket with excellent economics.

R2 does this

  • Stores source files and HLS output at 0.015 USD per GB-month.
  • Serves those objects over your own hostname with Cloudflare's cache in front.
  • Charges nothing for direct internet egress, however often a file is watched.
  • Speaks an S3-compatible API, so existing upload and lifecycle tooling mostly works.

R2 does not do this

  • Transcode. No encoder ships with object storage.
  • Package HLS or DASH. Segments and manifests have to arrive already built.
  • Restrict a playback session. A presigned URL signs one object, not a stream.
  • Carry production traffic on r2.dev. Cloudflare rate-limits that subdomain and documents it as development only.

The setup that works

Four pieces, in this order

Skip any one of them and you get a specific, recognisable failure. The diagram shows which.

  1. 1

    Private bucket

    Holds the source and the HLS output

  2. 2

    Encoder

    Builds the rendition ladder before upload

  3. 3

    Your own hostname

    Puts Cloudflare's cache in front of the bucket

  4. 4

    Player, and an edge that authorizes

    Reads the manifest, and gates it when playback is restricted

Each piece names what breaks in its absence.

1. Private bucket
Public r2.dev is rate-limited and cannot use Cloudflare's cache, access management, or Bot Management. Those are documented product limits, not preferences.
2. Encoder
Nothing plays. A common starting ladder is 1080p near 5 Mbps, 720p near 2.8 Mbps, and 480p near 1.4 Mbps in 6-second segments. Keep scaled dimensions divisible by two and never upscale past the source.
3. Your own hostname
Every segment request reaches R2, and viewers hit the r2.dev rate limit. This is the step that turns segment requests into cache hits.
4. Player, and an edge that authorizes
Public video still plays; restricted video cannot exist. Most published examples leave this piece marked TODO.

The arithmetic

Run the numbers before you fear them

HLS turns one video into thousands of small objects, and every fetch of one is a Class B operation at 0.36 USD per million requests. Six-second segments mean roughly 600 segment requests per viewer-hour, per rendition. Here is what that is worth at 1,000 viewer-hours of a single rendition.

  • R2, cache warm $0.02

    60,000 requests reach R2

  • R2, long tail $0.11

    300,000 requests reach R2

  • R2, no cache $0.22

    600,000 requests, plus rate limiting

  • Cloudflare Stream $60.00

    60,000 delivered minutes

1,000 viewer-hours of one hour of video. Cloudflare Stream shown at its published 1 USD per 1,000 delivered minutes.

So the operations bill is not the constraint

Even the uncached column costs less than a quarter. Two real costs sit behind it. Storage: one hour of 8 Mbps source is about 3.6 GB, and a 1080/720/480 ladder adds about 4.2 GB, so keeping both runs near 0.12 USD per month per hour of video. Availability: without your own hostname you are not paying more, you are getting rate-limited. Skipping the cache is an outage risk, not a billing risk.

Assumes one request per segment and a single rendition. Multiply by the renditions a session switches between. R2's free tier absorbs the first 10 million Class B operations each month, which is why small pilots report a bill of zero and cannot tell you what happens next.

developers.cloudflare.com/r2/pricing · developers.cloudflare.com/stream/pricing · developers.cloudflare.com/r2/buckets/public-buckets

Run it on your own library

Restricted playback

Why presigned URLs do not work for HLS

This is the wall almost every do-it-yourself write-up hits and then stops at.

  • A presigned URL authorizes exactly one object for a window of time. An HLS session is a manifest plus hundreds of separate segment objects.
  • The manifest lists segment paths as plain text. Signing the manifest leaves every segment it points at open, and signing every segment means rewriting the manifest per viewer per session.
  • The workable pattern authorizes at the edge instead: the player carries a short-lived token, a Worker in front of the bucket validates it on each request, and the bucket stays private.
  • Decide this before you publish. Moving from public objects to authorized playback afterwards means every URL already handed out has to be treated as leaked.

The question nobody answers

What Cloudflare's terms actually say

The search results for this are full of people asking and nobody checking, so here is what we did. On 2026-08-27 we read the Self-Serve Subscription Agreement at cloudflare.com/terms, effective 2025-09-12. Section 2 runs: 2.1 Access to Services, 2.2 Use of Services, 2.3 Credentials, 2.4 Subscription Terms, 2.5 Customer Content and Network Data, 2.6 Free and Trial Services, 2.7 Acceptable Use. The old limitation on serving non-HTML content, still quoted in forum threads, is not among them.

  • R2 is a paid product with published per-GB and per-operation pricing, and Cloudflare's R2 documentation describes attaching a custom domain for production traffic.
  • Section 2.7, Acceptable Use, still governs what the video is and how it was obtained.
  • Terms change and this page is a snapshot. Open the agreement, check its effective date, and read section 2 yourself before you build on it.

Cloudflare Self-Serve Subscription Agreement

This is a record of what we read on a stated date. It is not legal advice.

Failure modes

Six symptoms and what actually causes them

What you see What it is
Playback is fine for you, 429s for everyone else r2.dev in production. Cloudflare rate-limits it and documents it as development only.
The manifest downloads but nothing plays Missing Content-Type. An .m3u8 needs application/vnd.apple.mpegurl and a .ts needs video/mp2t. A generic octet-stream stops most players.
It fails only in the browser No CORS headers on the bucket hostname. hls.js fetches segments over XHR and is subject to CORS, while a native video element pointed at the same file is not.
Seeking is slow or broken on a raw .mp4 Progressive MP4 needs HTTP range requests and a moov atom at the front. This is what people hit when they put the mp4 in the bucket unchanged.
The operations count is far higher than expected Cache never engaged. Confirm traffic arrives through your own hostname and that the cache rule matches the segment paths, not only the manifest.
Renditions come out at odd sizes or upscaled An ffmpeg scale filter with no divisible-by-two guard and no cap at the source resolution.

Two questions this page deliberately leaves open

It explains how the architecture behaves, not which one you should pick or what your own library would cost. Those have their own pages.

Build it, buy the managed layer, or skip the bucket

Compare the three

What your hours of video and expected viewing actually cost

Open the calculator

NULX runs this setup on storage you already own. The free plan includes two hours of encoding.

Start on the free plan