---
title: "12 — Stream: Video Upload, Encoding, and Delivery"
uid: stream
tags: ["cloudflare", "encoding", "roadmap:cloudflare", "video", "stream", "cdn", "vod", "live-streaming"]
excerpt: "Stream is a managed pipeline — upload, encode for adaptive bitrate, store, deliver through the CDN — so application code treats a video as an opaque ID."
date: 2026-08-13T03:28:17+0000
source: https://www.aveshina.my.id/en/blog/stream
---

"Video hosting on Cloudflare, somehow" was my Stream summary, and it hid why the product exists. The model that pinned it: **Stream is a managed pipeline that takes a raw video upload and produces adaptive, globally-delivered playback — encoding, storage, and CDN delivery are all handled, so the application code treats a video as an opaque ID.** [1] The complexity of video (codecs, bitrates, resolutions, formats, player compatibility) is the whole reason this product exists; the application shouldn't have to touch any of it.

The framing that landed is what the pipeline replaces. Doing video "by hand" means: encode the source into multiple resolutions and formats, serve them from object storage, wire up a player that picks the right variant based on the viewer's bandwidth, handle range requests for seeking, and cache it globally so a viewer in Tokyo doesn't pull bytes from Virginia. Each of those is a project on its own. Stream collapses all of them into "upload a video, get a playback ID" [1][2].

```figure
<svg viewBox="0 0 740 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="The Stream pipeline. A raw video upload enters from the left. The Stream box encodes it into multiple resolutions (1080p, 720p, 480p), stores them, and delivers via Cloudflare's CDN to different devices on the right. A separate live-stream input branches off at the top.">
  <defs>
    <marker id="sarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- live input -->
    <rect x="40" y="30" width="110" height="34" rx="6" fill="#fee2e2" stroke="#dc2626"/>
    <text x="95" y="51" font-size="10" font-weight="700" fill="#7f1d1d" text-anchor="middle">live input (OBS)</text>
    <path d="M150,47 C200,47 200,90 240,100" fill="none" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#sarrow)"/>

    <!-- upload -->
    <rect x="40" y="110" width="110" height="40" rx="6" fill="#e0e7ff" stroke="#6366f1"/>
    <text x="95" y="134" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">raw upload</text>

    <!-- stream pipeline -->
    <rect x="230" y="90" width="240" height="100" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="350" y="112" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">Stream pipeline</text>

    <rect x="245" y="122" width="65" height="24" rx="4" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="277" y="138" font-size="9" font-weight="700" fill="#422006" text-anchor="middle">encode</text>
    <rect x="318" y="122" width="55" height="24" rx="4" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="345" y="138" font-size="9" font-weight="700" fill="#422006" text-anchor="middle">store</text>
    <rect x="381" y="122" width="75" height="24" rx="4" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="418" y="138" font-size="9" font-weight="700" fill="#422006" text-anchor="middle">deliver</text>

    <text x="350" y="164" font-size="9" fill="#052e16" text-anchor="middle">1080p · 720p · 480p · adaptive</text>
    <text x="350" y="180" font-size="9" font-style="italic" fill="#052e16" text-anchor="middle">→ playback ID</text>

    <path d="M150,130 L228,130" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>

    <!-- devices -->
    <rect x="540" y="80" width="80" height="34" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="580" y="101" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">desktop</text>
    <rect x="630" y="80" width="80" height="34" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="670" y="101" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">mobile</text>
    <rect x="540" y="160" width="80" height="34" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="580" y="181" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">tablet</text>
    <rect x="630" y="160" width="80" height="34" rx="6" fill="#fce7f3" stroke="#db2777"/>
    <text x="670" y="181" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">TV</text>

    <path d="M470,115 L538,97" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <path d="M470,130 L628,97" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <path d="M470,150 L538,177" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>
    <path d="M470,165 L628,177" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#sarrow)"/>

    <text x="370" y="225" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">the application code treats a video as an opaque playback ID — codecs and bitrates are not its problem</text>
    <text x="370" y="243" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">delivered through Cloudflare's CDN, so playback is fast worldwide</text>
  </g>
</svg>
```

## Video delivery

The core pipeline is end-to-end [2][3]:

- **Encoding.** A raw upload is transcoded into multiple resolutions and bitrates automatically — typically a ladder from low-resolution (for poor bandwidth) up to full HD or 4K. This is the "adaptive bitrate" set.
- **Storage.** The encoded variants are stored by Stream; I don't manage the buckets or the files.
- **CDN delivery.** Playback pulls from Cloudflare's global CDN, so a viewer anywhere gets the bytes from a nearby edge.
- **Player.** Stream provides a customizable player, or I can use a standard player pointed at the playback URL.

The application-facing surface is a playback ID. Embed the player with that ID, or hit the playback URL, and Stream handles the rest — including the adaptive bitrate negotiation that switches the viewer between resolutions as their bandwidth changes.

## Live streaming

Stream also handles live video [4]. A broadcaster sends a live feed from a tool like OBS Studio or Wirecast to an ingest endpoint; Stream takes care of transcoding the live feed into adaptive variants, distributing them through the CDN, and serving them to viewers in real time. From the application's perspective, a live stream has a playback ID the same way a recorded video does — the difference is just that the bytes are arriving live rather than from storage.

This collapses the barrier to "we want to broadcast an event to a global audience." The encoding, the global distribution, the player — all of it is the same pipeline whether the content is on-demand or live.

## Video processing

Two layers of processing are worth distinguishing [5]:

- **Automatic, built-in.** Stream's encoding/transcoding for adaptive bitrate is automatic and unconditional — every upload gets the multi-resolution treatment. That's the baseline.
- **Custom, via Workers.** For deeper customization — watermarking, metadata injection, intercepting video requests — a Worker can sit in front of Stream the same way it sits in front of R2. The Worker intercepts the request, does its transformation, and returns the (possibly modified) response. This is the "video processing via Workers" pattern.

The split matters because the built-in processing covers the common cases (encode, deliver, seek) for free, and the Worker-based processing is reserved for the cases where I genuinely need to modify the stream — not for basic serving, which Stream already handles.

## How Stream relates to R2

Stream is the higher-level product; R2 is the raw layer underneath it. For a video-heavy application, the question is how much of the pipeline I want to own:

- **Stream** — give me playback, handle the codecs. I treat videos as IDs.
- **R2 + a Worker** — I store the raw files and build the serving logic myself. More control, more work, and I'm now responsible for encoding, adaptive bitrate, and player behavior.

The default for almost any video use case is Stream. R2 + a Worker becomes the answer only when the video pipeline needs something Stream doesn't provide — a custom encoding step Stream can't do, a non-standard delivery mechanism, or a cost shape that favors owning the raw storage. For everyone else, Stream's "upload and forget" model is the right call.

## How I use this

The rule I keep: any video that needs to be played back by humans goes through Stream, full stop. The application stores the playback ID alongside whatever metadata it has (title, owner, permissions), and the actual bytes, codecs, and delivery are Stream's problem. When I find myself reaching for R2 to store video directly, that's the signal to ask what I'm trying to do that Stream doesn't already handle — and most of the time, the answer is "nothing, I should just use Stream."

## References

[1] Cloudflare, "Cloudflare Stream — Cloudflare Documentation," Cloudflare Developer Platform. [Online]. Available: [https://www.cloudflare.com/developer-platform/products/cloudflare-stream/](https://www.cloudflare.com/developer-platform/products/cloudflare-stream/)

[2] Cloudflare, "Delivering videos with Cloudflare," Cloudflare Support Docs. [Online]. Available: [https://developers.cloudflare.com/support/more-dashboard-apps/cloudflare-stream/delivering-videos-with-cloudflare/](https://developers.cloudflare.com/support/more-dashboard-apps/cloudflare-stream/delivering-videos-with-cloudflare/)

[3] Cloudflare, "Stream delivery — Cloudflare Stream," Cloudflare Application Services. [Online]. Available: [https://www.cloudflare.com/application-services/solutions/stream-delivery/](https://www.cloudflare.com/application-services/solutions/stream-delivery/)

[4] Cloudflare, "Serverless live streaming with Cloudflare Stream," Cloudflare Blog. [Online]. Available: [https://blog.cloudflare.com/stream-live/](https://blog.cloudflare.com/stream-live/)

[5] Cloudflare, "VOD platform — video-on-demand streaming software," Cloudflare Developer Platform. [Online]. Available: [https://www.cloudflare.com/developer-platform/solutions/video-on-demand/](https://www.cloudflare.com/developer-platform/solutions/video-on-demand/)

```quiz
Q: What does Cloudflare Stream handle that an application would otherwise have to build itself?
- Only the video storage; the app still encodes and serves
- The full pipeline: upload, encoding for adaptive bitrate, storage, CDN delivery, and a player
- Only the player UI; everything else is the app's job
correct: 1
explain: Stream is end-to-end. The application treats a video as a playback ID; encoding, storage, global delivery, and player are all handled. That's the whole point of the product.

Q: What is "adaptive bitrate" and why does it matter?
- A way to compress videos so they take less storage
- Encoding a video into multiple resolutions so the player can switch based on the viewer's bandwidth
- A billing model where you pay per bit transferred
correct: 1
explain: Adaptive bitrate means the video is encoded as a ladder of resolutions. The player picks the highest quality the viewer's current bandwidth can sustain, switching up or down as conditions change — preventing buffering.

Q: How does live streaming on Stream differ from on-demand video?
- Live uses a completely different playback API
- The ingest is live (from OBS/Wirecast), but the application still treats it as a playback ID — the pipeline is the same
- Live streams cannot be recorded
correct: 1
explain: The application-facing surface is identical: a playback ID. The difference is whether the bytes come from storage (on-demand) or arrive in real time (live). Encoding, distribution, and player are the same pipeline.

Q: When does it make sense to serve video directly from R2 + a Worker instead of Stream?
- Whenever you want any video playback
- When you need a custom encoding or delivery step Stream doesn't provide — otherwise, Stream is the default
- When you want to save money on storage
correct: 1
explain: R2 is the raw layer underneath. Building the pipeline yourself means owning encoding, adaptive bitrate, and player behavior. That's only worth it when Stream genuinely can't do what you need; otherwise Stream's "upload and forget" model wins.

Q: A Worker sits in front of a Stream playback request and adds a watermark. What layer of processing is this?
- The automatic built-in encoding step
- Custom Worker-based processing — the built-in encoding is separate and still runs on the original upload
- A CDN-level cache rewrite
correct: 1
explain: There are two processing layers: the automatic adaptive-bitrate encoding (unconditional, on every upload) and custom Worker-based transformation (intercepting requests to watermark, inject metadata, etc.). The watermark is the latter.
```
