# Zero egress fees, and the workloads where R2 actually beats S3

> Egress is the line that surprises people on an S3 bill. R2 charges nothing for it and charges for operations instead: here is the arithmetic for deciding, including where R2 loses.

You store 500 GB of user-uploaded video and serve about 20 TB a month. On S3
that is roughly $11 of storage and around $1,700 of egress. The storage line is
a rounding error; the transfer line is the bill.

That single asymmetry is why R2 exists, and it is worth understanding precisely
rather than as a slogan, because there are workloads where R2 is the more
expensive choice.

## The two pricing shapes

**S3 (and most object stores):** cheap storage, cheap operations, expensive
egress. Roughly $0.023/GB/month stored, and $0.09/GB out to the internet after
the first free tier. Traffic through CloudFront is cheaper but still charged.

**R2:** slightly cheaper storage, no egress charge at all, and operations priced
to matter. About $0.015/GB/month stored; class A operations (writes, lists)
about $4.50 per million after a free million; class B (reads) about $0.36 per
million after ten million free. Egress is $0: to anywhere, including to another
cloud.

So the question is never "which is cheaper". It is "is my workload dominated by
bytes out or by request count".

## The arithmetic

Take that 500 GB / 20 TB video workload.

- **S3:** 500 GB × $0.023 = $11.50 storage, 20,000 GB × $0.09 = $1,800 egress.
  Call it **$1,811/month**.
- **R2:** 500 GB × $0.015 = $7.50 storage, $0 egress. Say 5 million reads a
  month: (5M − 10M free) → free. Call it **$7.50/month**.

Now a different shape: an app storing 10 GB of small JSON blobs, read 200
million times a month, serving 400 GB.

- **S3:** $0.23 storage, $36 egress, ~$80 in GET requests. Call it **$116**.
- **R2:** $0.15 storage, $0 egress, (200M − 10M) × $0.36/M = **$68** in class B.
  Call it **$68**.

R2 still wins there, but the gap is a fifth of the first example, and if the
same 200 million reads served only 40 GB (tiny objects, cache misses), R2's
operations line would dominate and the two would be close. Push it further (a
billion tiny reads) and R2 loses.

The rule of thumb: **R2 wins on bytes, S3 competes on requests.**

## Where R2 wins decisively

- **User-generated media served to the public.** Images, video, audio,
  downloads. Bytes dominate, and Cloudflare's cache in front of a custom domain
  means most requests never touch R2 at all.
- **Anything another cloud will read.** Egress to a different provider is where
  S3 pricing hurts most, and it is $0 on R2. This is the "data gravity" tax the
  zero-egress model exists to remove.
- **Backups and archives you might one day have to restore.** The cost of
  getting data *out* is the cost you discover during an incident.
- **Datasets, model weights, artefacts**: large objects, read repeatedly,
  possibly from outside Cloudflare.

## Where R2 loses, or does not help

- **Enormous request volumes on small objects.** Class A and B operations are
  real money at scale. Count requests before you migrate.
- **Deep AWS integration.** If Lambda, Athena, Glue and EventBridge already read
  your bucket, staying in S3 avoids cross-cloud latency and a pile of glue.
- **Features beyond the common S3 surface.** Object Lock, storage classes,
  Glacier-style archival, S3 Select, some checksum modes and ACLs are either
  absent or different. R2 is S3-*compatible*, not S3.
- **You are already inside CloudFront with a committed-use discount.** Negotiated
  egress rates change the arithmetic.

## The migration is mostly a config change

R2 speaks the S3 API, so the SDK stays:

```ts
const client = new S3Client({
  region: "auto", // R2 has one global region; a real AWS region fails the signature
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
  credentials: { accessKeyId, secretAccessKey },
});
```

Two things that catch people:

- **`region: "auto"`.** Sending `us-east-1` produces a signature error whose
  message does not mention the region.
- **Checksums.** Recent AWS SDK versions send additional integrity headers by
  default that some S3-compatible providers reject. If uploads start failing
  after an SDK upgrade with an opaque 400, this is the first thing to check.

For bulk data movement, Cloudflare's Super Slurper copies an existing bucket
across, and Sippy migrates lazily on first read: you pay S3 egress once, for
the objects that are actually requested.

## Do not forget the cache

R2's zero egress is the floor, not the ceiling of the saving. Put a custom
domain in front of a public bucket and Cloudflare's CDN serves repeat requests
from cache, which also removes them from your class B operations count.

Set the cache headers when you write the object:

```ts
await putObject({
  key,
  body,
  contentType: "image/webp",
  cacheControl: "public, max-age=31536000, immutable",
});
```

`immutable` is honest only if the key never changes meaning. Include a hash or a
uuid in the key so a new version is a new key, and you never have to purge
anything.

## Before you decide

Get four numbers for a real month:

1. GB stored (and the growth rate).
2. GB transferred out.
3. Write and list operations.
4. Read operations, and what fraction a CDN would absorb.

Then price both. If bytes out dominate, R2 is not a close call. If requests
dominate and the objects are tiny, do the multiplication properly before you
move anything: the migration is easy, but so is being surprised by an
operations bill that nobody modelled.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
