You can serve video straight from your own R2 bucket. R2 by itself will not do it.
Object storage stores bytes. Playback also needs an encoding ladder, a cacheable hostname, and a way to restrict a stream. This page is the four pieces that work together, the cost of each at Cloudflare's published prices, and the six ways the setup breaks once real viewers arrive.
The four questions, answered first
- Is it allowed?
- We re-read Cloudflare's Self-Serve Subscription Agreement on 2026-08-27. The clause people still cite against video is not in it. Details and the link are further down.
- Does R2 transcode?
- No. There is no encoder in object storage. Something else has to build the renditions and the manifest before anything reaches the bucket.
- Can I expire a playback link?
- Not with an S3 presigned URL. HLS fetches every segment as its own request and the manifest holds plain paths, so signing one URL signs nothing that matters.
- What does it cost?
- Storage at 0.015 USD per GB-month, zero egress, and Class B operations on segment requests that miss cache. The third line turns out to be the small one.
Last reviewed 2026-08-27 by the NULX engineering team. Every price on this page 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, no matter how many times 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. They are listed in the failures table below.
-
1. A private bucket
Keep the bucket private. Cloudflare's own documentation says public r2.dev access is rate-limited and for development purposes, and that cache, access management, and Bot Management require a custom domain. Those are product limits, not style preferences.
-
2. An encoder that writes HLS into it
Build the rendition ladder before upload. A common starting point is 1080p near 5 Mbps, 720p near 2.8 Mbps, and 480p near 1.4 Mbps, cut into 6-second segments. Keep scaled dimensions divisible by two and never upscale past the source.
-
3. Your own hostname in front
Attach a custom domain to the bucket. This is the step that converts segment requests into cache hits, and cache hits are what keep both the operations count and the latency flat.
-
4. A player, and an edge that authorizes
hls.js or a native HLS player reads the manifest. If playback has to be restricted, a Worker in front of the bucket authorizes the request before the segment is returned. This is the piece most published examples leave 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 actually worth at 1,000 viewer-hours of a single rendition.
| Setup | Requests reaching R2 | Class B cost |
|---|---|---|
| Custom domain, cache warm (90% hit) | ~60,000 | ~0.02 USD |
| Custom domain, long tail (50% hit) | ~300,000 | ~0.11 USD |
| No cache in front (r2.dev) | ~600,000 | ~0.22 USD, and rate limiting |
Even the uncached column costs less than a quarter. Two real costs sit behind it. First, 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 is near 0.12 USD per month per hour of video. Second, availability: without a custom domain you are not paying more, you are getting rate-limited. Skipping the cache is an outage risk, not a billing risk.
1,000 viewer-hours of one hour of video. On R2: about 0.12 USD storage, 0 USD egress, and about 0.22 USD of operations at worst, so roughly 0.34 USD. On Cloudflare Stream at 1 USD per 1,000 delivered minutes, 1,000 viewer-hours is 60,000 minutes, so 60 USD, plus 0.30 USD stored. The gap is not a rounding difference, and it is the entire reason this architecture keeps getting rebuilt.
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
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 is an edge that authorizes instead: the player carries a short-lived token, a Worker in front of the bucket validates it on each request, and the bucket itself stays private.
- Decide this before you publish. Moving from public objects to authorized playback after the fact means every URL already handed out has to be treated as leaked.
The question nobody answers
Is serving video from R2 against Cloudflare's terms?
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 published 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 that is 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 itself describes attaching a custom domain for production traffic.
- Section 2.7, Acceptable Use, still governs what the video is and how it is 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 just put the mp4 in the bucket. |
| The operations count is far higher than expected | Cache never engaged. Confirm traffic arrives through the custom domain and that the cache rule matches the segment paths, not just 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. |
Choosing
Build it, buy the managed layer, or skip the bucket
All three are defensible. They differ in what you operate, not in whether they work.
| Build it on R2 | NULX on your R2 | Cloudflare Stream | |
|---|---|---|---|
| Who builds the encoder | You | NULX | Included |
| Who holds the files | You | You | Cloudflare |
| Cost at 1,000 viewer-hours | ~0.34 USD | ~0.34 USD plus your plan | ~60 USD |
| Restricted playback | Your Worker | Built in | Signed URLs |
| Time to first playback | Days to weeks | An afternoon | Minutes |
- If you already run an encoder and a Worker that authorizes segments, NULX sells you convenience you have finished building.
- Stream is genuinely simpler. One API, no bucket, no ladder decisions. If you never want to think about a segment again, the 60 USD buys that.
- NULX offers no formal uptime SLA or service credits today. If your contract needs one, this is not the product yet.
FAQ
Questions people search for
- Can you stream video directly from a Cloudflare R2 bucket?
- Yes, once the HLS output already exists in the bucket and a custom domain is in front of it. R2 serves the manifest and segments like any other objects. What R2 does not do is create those objects, so an encoder has to run before upload.
- 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.
- Why can I not use a presigned URL for HLS playback?
- A presigned URL authorizes a single 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 with a short-lived token validated by a Worker.
- Is R2 cheaper than Cloudflare Stream for video?
- For delivery-heavy libraries, substantially. At 1,000 viewer-hours of one hour of video, R2 is roughly 0.34 USD in storage and operations with no egress charge, while Stream at 1 USD per 1,000 delivered minutes is about 60 USD. Stream includes encoding and packaging that you otherwise have to run.
- Do I need Cloudflare Workers to serve video from R2?
- Only for restricted playback. Public video needs a custom domain and a cache rule. A Worker enters the picture when a request has to be authorized before the segment is returned.
See the number for your own library
The calculator takes your hours of video and expected viewing and separates what Cloudflare bills you directly from what NULX bills. The free plan includes two hours of encoding and connects to a bucket you already own.