---
title: "04 — R2 Storage: Object Storage Without Egress Fees"
uid: r2-storage
tags: ["s3", "object-storage", "cloudflare", "roadmap:cloudflare", "egress", "media", "r2"]
excerpt: "R2 is S3-compatible object storage with zero egress fees — a pricing model that is the feature, flipping whole app categories from 'too expensive' to 'trivial.'"
date: 2026-08-13T03:28:19+0000
source: https://www.aveshina.my.id/en/blog/r2-storage
---

"S3 but cheaper" was my R2 summary, and it undersold what the pricing actually does. The idea the whole product is built around: **R2 is S3-compatible object storage whose structural innovation is zero egress fees — and that single change flips entire categories of application from 'too expensive to run' to 'trivial.'** [1] The pricing model _is_ the feature.

The framing that landed is the unit economics of traditional object storage. With S3 and its peers, storage is cheap and writes are cheap, but every byte read back out — every image served, every video streamed, every backup downloaded — is billed at a per-GB egress rate. For a media-heavy app, egress dominates the bill, often by an order of magnitude. R2 removes that line item [1][2]. Store a terabyte, serve it a million times, pay for the terabyte of storage and effectively nothing for the serving.

```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="Cost comparison between S3 and R2. Both charge for storage and writes. The difference is the egress column: S3 bills per GB read out, a large red bar; R2 has no egress fee, the column is absent or zero green.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- headers -->
    <text x="185" y="24" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">S3-style pricing</text>
    <text x="555" y="24" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">R2 pricing</text>

    <!-- S3 bars -->
    <rect x="80" y="50" width="80" height="30" rx="4" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="120" y="69" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">storage</text>
    <rect x="170" y="50" width="80" height="30" rx="4" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="210" y="69" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">writes</text>
    <rect x="260" y="40" width="120" height="50" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="2"/>
    <text x="320" y="69" font-size="11" font-weight="700" fill="#7f1d1d" text-anchor="middle">EGRESS (per GB)</text>

    <text x="185" y="110" font-size="10" font-style="italic" fill="#7f1d1d" text-anchor="middle">egress dominates the bill</text>

    <!-- R2 bars -->
    <rect x="450" y="50" width="80" height="30" rx="4" fill="#dcfce7" stroke="#16a34a"/>
    <text x="490" y="69" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">storage</text>
    <rect x="540" y="50" width="80" height="30" rx="4" fill="#dcfce7" stroke="#16a34a"/>
    <text x="580" y="69" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">writes</text>
    <rect x="630" y="55" width="60" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-dasharray="3,2"/>
    <text x="660" y="69" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">FREE</text>

    <text x="555" y="110" font-size="10" font-style="italic" fill="#052e16" text-anchor="middle">read as much as you want</text>

    <!-- bottom note -->
    <text x="370" y="180" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">the same data, served through Cloudflare's network</text>
    <text x="370" y="205" font-size="10" fill="#64748b" text-anchor="middle">S3-compatible API — tools and SDKs that target S3 work against R2 unchanged</text>
    <text x="370" y="225" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">for a media app, the bill stops scaling with popularity</text>
  </g>
</svg>
```

## Object storage fundamentals

Object storage is the layer for **unstructured data** — files, blobs, media — as opposed to the structured rows of a database or the key-value pairs of KV [2]. The unit is an _object_ (a file plus its metadata), stored in a _bucket_, addressed by a key. There's no query language, no joins, no schema — just put, get, list, delete.

R2 implements the S3 API, which is the part that makes it drop-in. Anything written for S3 — the AWS SDK, the aws-sdk npm package, tools like rclone and the AWS CLI — works against R2 with a different endpoint URL and different credentials [1]. The way of thinking transfers wholesale; the code transfers almost wholesale.

## Prerequisites that matter

Two prerequisites from the reference are worth flagging because they shape how I work with R2 in practice:

- **Node.js and npm.** R2 is most often accessed from Workers, but the tooling around it — upload scripts, migration utilities, the AWS SDK when used outside Workers — is Node-shaped. Understanding npm and being comfortable in a Node REPL makes the surrounding workflow tractable [3].
- **TypeScript.** Workers pair naturally with TypeScript, and R2 bindings are typed. Defining the shape of an upload's metadata, the structure of a manifest object, the options on a get — TypeScript turns what would be runtime errors into editor squiggles [4].

## Serving assets through R2

The canonical pattern is R2-as-origin-behind-a-Worker. Static assets — images, videos, build artifacts, user uploads — live in a bucket, and a Worker sits in front, reading from the R2 binding and returning a Response [5]:

```
const object = await env.MY_BUCKET.get("images/avatar.png");
if (!object) return new Response("Not found", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
return new Response(object.body, { headers });
```

Because the Worker runs at the edge and R2 is integrated with Cloudflare's network, the response is served from near the user with no egress charge. For a static site or a media library, this replaces both a hosting provider and its bandwidth bill.

## Large file handling

R2 and the Workers platform handle large files with two complementary techniques [6]:

- **Streaming.** Rather than buffering a whole file into memory, you process it as a ReadableStream. A Worker can pipe a large upload directly to R2 or stream a large response back out, never holding the full bytes.
- **Range requests.** For media playback, browsers ask for byte ranges (Range: bytes=0-1048575). R2 supports range gets natively, so a Worker can serve just the requested chunk — the basis for video seeking and adaptive streaming.

For genuinely huge video files, Cloudflare Stream (covered separately) is the higher-level product: it handles encoding, adaptive bitrate, and playback for you, with R2 underneath.

## Bucket operations and lifecycle

Buckets are the top-level container. The lifecycle is straightforward [7][8]:

- **Create** a bucket to house a category of data (one per environment, per service, per tenant).
- **Use** it by uploading objects via the dashboard, wrangler, the S3 API, or a Worker.
- **Delete** a bucket only after emptying it — buckets must be empty before deletion.

One nuance the reference flags: R2 doesn't ship the rich lifecycle-rules system that S3 has (automatic tiered expiration, transition to cold storage). For object expiration today, the pattern is custom logic — a scheduled Worker that scans and deletes based on a stored timestamp, or relying on a higher-level product like Images or Stream that owns its own lifecycle [8].

## Asset management patterns

The two patterns I actually use:

- **Static asset storage for Pages.** A Pages project can use an R2 bucket as its asset layer — uploads and large build artifacts live in R2, served through the Pages/Worker stack [5]. This is the natural home for anything too big to commit to Git.
- **On-the-fly transformation at the edge.** A Worker reads an image from R2, transforms it (resize, format conversion via cf.image), and serves the result — caching the transformed version. R2 holds the source of truth; the edge holds the derivatives. This is where zero egress pays off: you can serve a thousand different resized variants of one source image and pay for none of the reads.

## How I use this

R2 has become my default for any blob storage in a Workers project — the absence of egress removes the entire category of "can we afford to serve this?" anxiety. The pattern I keep returning to: one bucket per environment, a Worker in front that handles auth and content-type and caching, and a clear naming scheme for keys (media/{type}/{id}.{ext}). When I find myself wanting lifecycle rules or rich querying over the stored objects, that's the signal to move to a higher-level product (Images, Stream) rather than fight the raw bucket.

## References

[1] Cloudflare, "Cloudflare R2 | Zero egress fee object storage," Cloudflare Developer Platform, 2024. [Online]. Available: [https://www.cloudflare.com/developer-platform/products/r2/](https://www.cloudflare.com/developer-platform/products/r2/)

[2] Cloudflare, "What is object storage?," Cloudflare Learning, 2024. [Online]. Available: [https://www.cloudflare.com/learning/cloud/what-is-object-storage/](https://www.cloudflare.com/learning/cloud/what-is-object-storage/)

[3] Node.js Foundation, "Node.js documentation," nodejs.org. [Online]. Available: [https://nodejs.org/en/](https://nodejs.org/en/)

[4] TypeScript, "TypeScript documentation," typescriptlang.org. [Online]. Available: [https://www.typescriptlang.org/docs/](https://www.typescriptlang.org/docs/)

[5] Cloudflare, "Use R2 as static asset storage with Cloudflare Pages," Cloudflare Pages Tutorials. [Online]. Available: [https://developers.cloudflare.com/pages/tutorials/use-r2-as-static-asset-storage-for-pages/](https://developers.cloudflare.com/pages/tutorials/use-r2-as-static-asset-storage-for-pages/)

[6] Cloudflare, "Cloudflare R2 limits," Cloudflare Docs. [Online]. Available: [https://developers.cloudflare.com/r2/platform/limits/](https://developers.cloudflare.com/r2/platform/limits/)

[7] Cloudflare, "Buckets · Cloudflare R2," Cloudflare Docs. [Online]. Available: [https://developers.cloudflare.com/r2/buckets/](https://developers.cloudflare.com/r2/buckets/)

[8] Cloudflare, "Bucket lifecycle · Cloudflare R2," Cloudflare API Reference. [Online]. Available: [https://developers.cloudflare.com/api/resources/r2/subresources/buckets/subresources/lifecycle/](https://developers.cloudflare.com/api/resources/r2/subresources/buckets/subresources/lifecycle/)

```quiz
Q: What is the structural pricing difference between R2 and traditional S3-style storage?
- R2 charges more for storage to compensate for cheaper writes
- R2 has zero egress fees — you don't pay per GB read out
- R2 doesn't charge for storage at all, only for writes
correct: 1
explain: Storage and writes are priced comparably to S3; the structural difference is that reads (egress) are free. For a media app, that removes the cost that scaled with popularity.

Q: R2 implements which API, making it compatible with existing tools and SDKs?
- A custom Cloudflare-only API
- The S3 API — existing S3 tooling works against R2 with different credentials
- The Google Cloud Storage API
correct: 1
explain: R2 is S3-compatible. The AWS SDK, AWS CLI, rclone, and similar tools work against R2 unchanged, just pointed at a different endpoint.

Q: How does a Worker efficiently serve a large video from R2 with seeking support?
- Buffer the entire video into the Worker's memory first
- Use range requests to serve only the requested byte chunk, streamed
- Convert the video to base64 and inline it in the response
correct: 1
explain: R2 supports range gets natively. Browsers request byte ranges for seeking, and the Worker streams just that chunk — never holding the full file.

Q: You want automatic tiered expiration (move objects to cold storage after 30 days). What's true on R2 today?
- R2 has the same lifecycle-rules system as S3
- R2 doesn't ship rich lifecycle rules — you implement expiration with custom logic, or use a higher-level product
- R2 expires all objects automatically after 30 days
correct: 1
explain: R2 doesn't have S3's full lifecycle-rules system. For expiration today, you write a scheduled Worker that scans and deletes, or you use Images/Stream which own their lifecycle.

Q: A Worker reads an image from R2, resizes it, caches the result at the edge, and serves it. The source image is read from R2 once and served as 500 resized variants. The egress cost is…
- 500× the per-GB rate
- Zero — R2 doesn't charge for reads, regardless of count
- The storage cost of 500 cached variants
correct: 1
explain: This is the pattern R2 was built for. The source is stored once, read many times, transformed at the edge, and none of the reads incur egress fees.
```
