The system
How the system worksThe DirectoryDispatchThe LedgerAI modelsAI agentsYour installationThe appThe research libraryWhat changes for your businessUse it from your softwareEverything it can do
Messaging
iMessageWhatsApp
Ads and leads
AdsCreativeLeads
Devices
Devices
Industries
RealtorsRestaurantsHome services
Explore
How it worksPermissions and limitsCompareSet upQuestionsText me

R2 cuts a 10 TB delivery bill from $923 to $18.45

Cloudflare R2 pricing, S3 arithmetic, missing versioning, exact Worker code, and measured production behavior for deciding where files belong.

Cloudflare R2 is object storage: give it a key like img/up/hero.png and some bytes, and it hands them back on request. It speaks two dialects: the Amazon S3 HTTP API, so existing S3 tools work against it, and a native binding inside a Cloudflare Worker where the bucket is a JavaScript object with put, get, list and delete. Objects go to 5 TiB, keys to 1,024 bytes, and a bucket holds any number of them.

The reason anyone brings it up is the price of getting bytes out. Amazon charges for that. Cloudflare does not. The rest of this page is the arithmetic that follows, plus the things R2 will not do for you.

Evidence status

Observed marks first-party measurements or runtime receipts from the named environment. Derived marks arithmetic calculated from cited inputs. Specified marks vendor or standards documentation. Implemented and deployed name code and live-state evidence, respectively. Reproduced means the stated procedure was rerun. Externally attested marks operator reports; those reports show that an experience occurred, not that it is universal.

Zero egress is real; the meter is on the operations

R2 bills three things: how much you store, and two classes of API call. Class A operations change state: PutObject, CopyObject, ListObjects, CreateMultipartUpload, UploadPart, CompleteMultipartUpload. Class B operations read state: GetObject, HeadObject, HeadBucket. Deletes are free. Bandwidth to the internet is free.

Rates, read from the pricing page on 2026-07-26:

StandardInfrequent Access
Storage$0.015 / GB-month$0.01 / GB-month
Class A (writes, lists)$4.50 / million$9.00 / million
Class B (reads)$0.36 / million$0.90 / million
Data retrievalnone$0.01 / GB
Egress to the internetfreefree
Free each month10 GB-month, 1M Class A, 10M Class Bnone; the free tier is Standard only
Minimum storage durationnone30 days

Cloudflare rounds usage up to the next unit: 1,000,001 operations bills as two million, 1.1 GB-month bills as 2 GB-month.

A reader of that same page put the obvious objection plainly.

He is describing the meter correctly and drawing the wrong conclusion. Price the overage. Class B beyond the free 10 million costs $0.36 per additional million. On the ten-terabyte workload computed below, the S3 egress line is $891.00. For R2's metered operations to cost that much, you would have to make 2.475 billion Class B calls beyond the free allowance in one month: 891 ÷ 0.36 × 1,000,000. The class-ops meter is real. It is roughly three orders of magnitude away from being the thing that costs you money.

Serving 10 TB a month: $18.45 on R2, $923.00 on S3

The workload: 1,000 GB stored, average object 500 KB (so 2,000,000 objects), 200,000 new objects written during the month, and 10,000 GB served to the public internet, or 20,000,000 GET requests. S3 prices are us-east-1, pulled from the AWS Price List API on 2026-07-26: Standard storage $0.023/GB-month, Tier-1 requests (PUT, COPY, POST, LIST) $0.005 per 1,000, Tier-2 requests (GET and all others) $0.0004 per 1,000, data transfer out to the internet $0.09/GB for the first 10 TB beyond the 100 GB monthly free allowance.

LineR2 arithmeticR2S3 arithmeticS3
Storage(1,000 − 10 free) × $0.015$14.851,000 × $0.023$23.00
Writes200,000 of 1,000,000 free$0.00200,000 × $0.005/1,000$1.00
Reads(20,000,000 − 10,000,000) × $0.36/M$3.6020,000,000 × $0.0004/1,000$8.00
Egress10,000 GB, unmetered$0.00(10,000 − 100) × $0.09$891.00
Total$18.45$923.00

Fifty times cheaper, and the ratio is almost entirely one line: egress is 96.5% of the S3 bill. The table also shows that R2's operation rates are not a gimmick to claw the egress back. Class A at $4.50/million undercuts S3's Tier-1 at $5.00/million, and Class B at $0.36/million undercuts Tier-2 at $0.40/million. R2 is 10% cheaper per call and free on bandwidth.

Cloudflare's CTO stated the free-tier gap when R2 launched, and the allowances he named still hold on the page fetched today.

Two published totals for real public-serving workloads, both itemised. A 33 GB WordPress plugin mirror on R2 plus Workers, and a full archive of every SEC filing served at roughly twice the SEC's own volume:

Neither workload needed versioning, and neither was cold. That is why they land where they do.

The workload where S3 wins is the one you never read

R2 has exactly two storage classes. S3 has a ladder that goes much colder. For a write-once archive that is almost never read and never leaves the cloud, the egress advantage is worth nothing and the storage floor decides.

The workload: 100,000 GB (100 TB) of compliance records, written once, read a handful of times a year, consumed inside the same cloud.

Where it sitsRateMonthly
R2 Standard(100,000 − 10) × $0.015$1,499.85
R2 Infrequent Access100,000 × $0.01 (no free tier applies)$1,000.00
S3 Glacier Flexible Retrieval100,000 × $0.0036$360.00
S3 Deep Archive Access tier100,000 × $0.00099$99.00

R2's cheapest class costs 2.8× the Glacier Flexible rate and 10.1× the Deep Archive rate. There is no colder tier to move to; Standard and Infrequent Access are the whole ladder. If your bytes are cold and captive, stay on S3.

The person who ran both and stayed on AWS drew the boundary in one sentence.

Infrequent Access pays only below about one read every two months

Infrequent Access looks like a third off the storage price. The retrieval fee eats it almost immediately. Take a 1 GB object held for one month and read r times:

  • Standard: $0.015 + r × $0.00000036
  • Infrequent Access: $0.010 + r × $0.0000009 + r × 1 GB × $0.01

Set them equal: 0.005 = 0.01000054r, so r ≈ 0.5. The retrieval fee is 99.99% of the right-hand side; the operation-rate difference is noise. A 1 GB object must be read less than once every two months for Infrequent Access to be cheaper. Add the 30-day minimum billing duration, where you pay a full month even if you delete on day two, and the class is for backups and cold originals, nothing else.

Move objects there with a lifecycle rule rather than by hand. The transition itself is billed as a Class A operation.

bash
npx wrangler r2 bucket lifecycle add miscsubjects-ledger \
  --name "archive-old-events" \
  --prefix "events/" \
  --storage-class InfrequentAccess \
  --transition-days 30
Line 5, Column 1Shell Script

Lifecycle rules also delete: an expiration rule on a prefix is how an ingest bucket stops growing forever, and how incomplete multipart uploads get cleaned up. They expire after 7 days by default. A bucket accepts up to 1,000 rules.

R2 will not keep the old version of an object

This is the gap that costs people data. When you put to a key that already exists, the previous bytes are gone.

The S3 compatibility table marks PutBucketVersioning, GetBucketVersioning, PutObjectLockConfiguration and GetObjectLockConfiguration all unsupported. Versioning and Object Lock are the two standard defences against ransomware and against a human running the wrong script; R2 offers neither in S3's form.

It does have bucket locks, a per-prefix retention rule blocking deletion and overwriting for a fixed period or indefinitely, enforced with 10069 / ObjectLockedByBucketPolicy and HTTP 403. That stops deletion. It does not give you "fetch me yesterday's copy".

For that you write versioning yourself: never overwrite a key, always write a new one, keep a pointer.

js
// Content-addressed writes: the key carries the version, so nothing is ever overwritten.
export async function putVersioned(env, logicalKey, body, contentType) {
  const bytes = body instanceof ArrayBuffer ? body : new TextEncoder().encode(body);
  const digest = await crypto.subtle.digest('SHA-256', bytes);
  const hash = [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('');
  const versionKey = `${logicalKey}/${Date.now()}-${hash.slice(0, 12)}`;
  await env.R2.put(versionKey, bytes, { httpMetadata: { contentType } });
  // The pointer is one small object; reading it is one Class B call.
  await env.R2.put(`${logicalKey}/current`, versionKey, {
    httpMetadata: { contentType: 'text/plain' },
  });
  return { versionKey, hash };
}

export async function getCurrent(env, logicalKey) {
  const ptr = await env.R2.get(`${logicalKey}/current`);
  if (!ptr) return null;
  return env.R2.get(await ptr.text());   // second Class B call
}
Line 19, Column 1JavaScript

The cost of doing it this way: every read is two Class B operations instead of one, and old versions accumulate until a lifecycle rule expires them. At $0.36 per million reads, the doubled read is $0.36 per million objects fetched. That is the actual price of the missing feature.

The free-egress trust question, answered from the terms

The sharpest objection is not about the price list but about whether it is load-bearing.

The worry points at a real clause, and the clause says the opposite of what it assumes. Cloudflare's Service-Specific Terms restrict the CDN on Free, Pro and Business plans: "Unless you are an Enterprise customer, Cloudflare offers specific Paid Services (e.g., the Developer Platform, Images, and Stream) that you must use in order to serve video and other large files via the CDN." The Developer Platform named there is the thing R2 belongs to. Using R2 to serve large files is the compliant path, not the risky one.

The Developer Platform section carries its own limit, and it is a different kind: "Cloudflare may temporarily limit your storage and/or the number of requests you can make or receive using the Developer Platform if processing such requests would put an undue burden on the Cloudflare network." Rate limiting, stated in advance. Not a retroactive per-GB bill.

Verdict: the enforcement risk on R2 is throttling under abnormal load, not an undisclosed egress charge. The pricing page's footnote is unambiguous: "Egressing directly from R2, including via the Workers API, S3 API, and r2.dev domains does not incur data transfer (egress) charges and is free." What it does not cover is metered services you bolt on top; those bill separately.

Every call, and which meter it hits

Bind the bucket first. In wrangler.toml:

toml
[[r2_buckets]]
binding = "R2"
bucket_name = "miscsubjects-ledger"
Line 3, Column 1toml

binding is the variable name your code sees; bucket_name is the real bucket. Create the bucket before the first deploy, or the binding fails:

bash
npx wrangler r2 bucket create miscsubjects-ledger
npx wrangler r2 bucket list
Line 2, Column 1Shell Script
CallWhat it doesMeter
env.R2.put(key, value, opts)Stores bytes; returns an R2Object. Strongly consistent: once the promise resolves, every reader worldwide sees itClass A
env.R2.get(key, opts)Returns R2ObjectBody with .body as a stream, or null if absentClass B
env.R2.head(key)Metadata only, no body, or nullClass B
env.R2.list(opts)Up to 1,000 keys, lexicographic, opts.prefix to scopeClass A
env.R2.delete(key or key[])Up to 1,000 keys per callfree
env.R2.createMultipartUpload(key)Starts a multipart uploadClass A
upload.uploadPart(n, body)One part; all non-final parts must be the same size and ≥ 5 MiBClass A each
upload.complete(parts)Finishes itClass A

Note the trap in that table: list is a Class A operation, priced 12.5× a read. A paginated file browser that lists on every page view burns the expensive quota, not the cheap one.

A single put accepts up to 5 GiB. Above that, multipart, or you get 100100 / EntityTooLarge:

js
const upload = await env.R2.createMultipartUpload('big/archive.tar');
const parts = [];
let n = 1;
for (const chunk of chunksOf(stream, 16 * 1024 * 1024)) {   // 16 MiB, uniform
  parts.push(await upload.uploadPart(n++, chunk));
}
await upload.complete(parts);
Line 7, Column 1JavaScript

From outside a Worker, use the S3 API against https://<ACCOUNT_ID>.r2.cloudflarestorage.com with region: "auto", or hand a browser a presigned URL so it uploads straight to R2 without the bytes passing through your server:

js
import { AwsClient } from 'aws4fetch';

const r2 = new AwsClient({ accessKeyId: ACCESS_KEY_ID, secretAccessKey: SECRET_ACCESS_KEY });
const url = new URL(`https://${ACCOUNT_ID}.r2.cloudflarestorage.com/my-bucket/uploads/${name}`);
url.searchParams.set('X-Amz-Expires', '3600');           // seconds
const signed = await r2.sign(new Request(url, { method: 'PUT' }), {
  aws: { signQuery: true, service: 's3' },
});
// signed.url is safe to hand to a browser; it expires in one hour.
Line 9, Column 1JavaScript

Tamper with any signature parameter and the request fails with 10035 / SignatureDoesNotMatch; let it age out and you get 10018 / ExpiredRequest.

Serving an object publicly, and the header that decides your bill

Three ways to make a bucket readable from the internet. A custom domain puts it behind a hostname on your zone, the only option that gets Cloudflare Cache, WAF rules and Bot Management. An r2.dev subdomain is one toggle, documented as non-production. A Worker route gives you the object plus whatever logic you put in front of it. The third, in nine lines:

js
// functions/img/[[path]].js: every /img/* URL is an R2 key.
export async function onRequestGet(context) {
  const { params, env } = context;
  const key = 'img/' + (Array.isArray(params.path) ? params.path.join('/') : String(params.path || ''));
  if (!env.R2) return new Response('no R2', { status: 500 });
  const obj = await env.R2.get(key);
  if (!obj) return new Response('not found', { status: 404 });
  const ct = obj.httpMetadata?.contentType || 'image/png';
  return new Response(obj.body, { headers: { 'content-type': ct, 'cache-control': 'public, max-age=31536000' } });
}
Line 10, Column 1JavaScript

The URL path is the object key: no media table, no second name for a file, and a rename is a copy.

cache-control: public, max-age=31536000 is the line that matters financially. Cached responses never reach R2, so they cost no Class B operation. Fetched live, the header comes back on the wire:

text
$ curl -sI https://miscsubjects.com/img/up/cloudflare-os-r2-hero-card.png
HTTP/2 200
content-type: image/png
content-length: 51205
cache-control: public, max-age=31536000
cf-cache-status: MISS
cf-ray: a210b8bcbb715616-SJC
Line 7, Column 1Plain Text

A one-year max-age is only safe because a changed image is written under a new key, never patched in place. Serve mutable objects this way and you will serve stale bytes for a year.

Cache-hit ratio is not a rounding error on the bill:

When a row outgrows D1, the bytes move to R2 and the row keeps a pointer

D1, the SQLite database in the same account (D1 as the spine), has a hard per-value ceiling: 2,000,000 bytes for any string, BLOB or row. Exceed it and every write to that row fails with D1_ERROR: string or blob too big, the driver's rendering of SQLite's SQLITE_TOOBIG.

This application hit that wall storing article revision history. Each write snapshots the previous version — full body, claims and sources — into a JSON blob on the row. One article's metadata reached 2,068,258 bytes across 24 snapshots, 68,258 over the cap, and from then on every write to it returned HTTP 500.

The fix generalises into a rule: when a field outgrows its row, the field moves to object storage and the row keeps a pointer plus a hash. The pointer is small and fixed-size; the hash is what makes the pointer trustworthy.

The key format is one line in functions/_lib/revisions_r2.js:

js
const PREFIX = "revisions/";
function r2Key(slug, n) { return `${PREFIX}${slug}/${n}.json`; }
Line 2, Column 1JavaScript

So revision 3 of this article lives at revisions/cloudflare-os-r2/3.json. What stays in D1 is a nine-field index entry, written by the same function:

js
return {
  n: full.n, ts: full.ts, title: full.title, status: full.status,
  register: full.register, bytes: full.body.length,
  prev_hash: full.prev_hash, hash: full.hash,
  r2_key: key,
};
Line 6, Column 1JavaScript

prev_hash and hash are the point: the chain is verifiable from D1 alone, no R2 read needed to prove the history has not been edited, while the bytes are fetched only when someone asks for a specific revision. That 2,068,258-byte row became 206,362 bytes, and the original revision is still retrievable.

Measured across the bucket today: 4,930 objects under revisions/, 148,075,146 bytes, 1,053 distinct articles, mean 30,036 bytes per revision. The largest single history, revisions/bpc-157/, is 119 objects and 14,503,688 bytes — 7.25× the D1 row cap, so that article alone would be permanently unwritable under the old scheme.

Proof the offload preserved the history rather than truncating it:

bash
$ curl -s "https://miscsubjects.com/api/articles/cloudflare-os-r2?rev=0" | jq '{rev,is_head,prev_hash,body_len:(.body|length)}'
{ "rev": 0, "is_head": false, "prev_hash": "genesis", "body_len": 2878 }
Line 2, Column 1Shell Script

That body came out of R2. Nothing but the index is in the database.

Choosing between R2, S3, KV, D1 and Durable Object storage

WorkloadPut it inWhy, and the limit that decides it
Images, video, uploads, anything served to the public internetR2Free egress; 5 TiB per object; strongly consistent
Cold archive, rarely read, consumed inside AWSS3 Glacier$0.0036–$0.00099/GB-month against R2's $0.01 floor
A file with a legal retention requirement and a need to read yesterday's copyS3R2 has bucket locks but no object versioning
Small values read constantly from many places — flags, prompts, rendered snapshotsWorkers KVValues to 25 MiB, but eventually consistent and 1 write/second per key
Anything you need to query, join, filter or indexD110 GB per database, 2 MB per value; a bucket cannot answer WHERE
Per-entity state needing serialized writes and coordinationDurable Object storage10 GB per object, single-threaded execution (the Workers layer)
A row that has outgrown 2 MBR2, with a pointer in D1The offload pattern above
Blobs written and read once, under 25 MiB, with no need for a URLeither KV or R2R2 unless you need sub-millisecond reads at the edge

Two failure modes worth naming: KV used as a database (eventually consistent, so a read after a write may return the old value), and R2 used as a database (no query, only a lexicographic key scan at Class A prices).

Symptom, cause, fix

SymptomCauseFix
no R2 binding / no R2, HTTP 500The Worker deployed without the r2_buckets entry, or the bucket does not exist yetnpx wrangler r2 bucket create <name>, then confirm the [[r2_buckets]] block is present in every environment, including [[env.preview.r2_buckets]]
10006 / NoSuchBucket, HTTP 404Bucket name typo, or the bucket is in a different accountnpx wrangler r2 bucket list and compare byte for byte
get() returns null instead of throwingNot an error — the Workers API returns null for a missing key rather than raising NoSuchKeyBranch on null; do not wrap in try/catch and expect a throw
100100 / EntityTooLarge, HTTP 400Single-part upload over 5 GiBSwitch to createMultipartUpload; parts uniform and ≥ 5 MiB
10011 / EntityTooSmall or 10048 / InvalidPart, HTTP 400A non-final part under 5 MiB, or parts of differing sizesEvery part except the last must be ≥ 5 MiB and identical in size
10058 / TooManyRequests, HTTP 429More than one write per second to the same keyShard the key, or funnel writes for that key through a Durable Object
10035 / SignatureDoesNotMatch, HTTP 403A presigned URL was edited, or the secret is wrongRegenerate; check URL encoding of the key
10069 / ObjectLockedByBucketPolicy, HTTP 403A bucket lock retention rule covers that prefixWait out the retention period; a lock is not overridable
D1_ERROR: string or blob too big on a write that used to workA JSON column crossed D1's 2,000,000-byte per-value capOffload the heavy field to R2, keep a pointer and a hash in the row
wrangler r2 object put --file fails with fetch failedReported against Wrangler 3.74.0 and still openPipe the file instead: `cat f.txt \
Multipart complete() returns empty customMetadata in production but not in wrangler devOpen bug, reported against Wrangler 3.57.2Re-read with head() after completing; the metadata is stored, only the return value drops it

How the numbers on this page were measured

Four measurements, taken 2026-07-26 against the live production bucket from a laptop served by the San Jose edge (cf-ray suffix SJC).

1 — Buckets and inventory. npx wrangler r2 bucket list returns three buckets: loop-data-raw (2026-05-31), miscsubjects-ledger (2026-06-09), miscsubjects-store (2026-06-16). The inventory of miscsubjects-ledger was walked through the authenticated list route, 1,000 keys per page, following the cursor:

bash
curl -s -H "x-terminal-key: $TERMINAL_KEY" \
  "https://miscsubjects.com/api/r2/?list=1&limit=1000&cursor=$CURSOR"
Line 2, Column 1Shell Script

62 pages, 41,809 objects, 3,904,595,147 bytes (3.64 GiB) — under the 10 GB free allowance, so today's storage line is $0.00. By prefix: events/ 34,258 objects / 2,241 MB; revisions/ 4,930 / 148 MB; img/gen/ 387 / 765 MB; img/up/ 191 / 432 MB; docs/ 1,332 / 9.5 MB. The walk itself cost 62 Class A operations.

2 — Put and get latency. A 65,536-byte JSON object written and read six times each over one reused TLS connection, at the deliberately-named temporary key tmp/measure-2026-07-25-delete-me.json, then deleted — DELETE returned {"ok":true,...,"deleted":true} and a follow-up list of tmp/ returned zero objects.

bash
curl -s -X PUT "https://miscsubjects.com/api/r2/tmp/measure-2026-07-25-delete-me.json" \
  -H "x-terminal-key: $TERMINAL_KEY" -H 'content-type: application/json' \
  --data-binary @probe.json -w "%{time_total}\n"
Line 3, Column 1Shell Script

PUT round trips: 535, 587, 634, 737, 801, 2,471 ms — median 686 ms. GET: 96, 104, 125, 148, 185, 192 ms — median 136 ms. These are full client-to-edge-to-R2-to-client times through the Worker, not R2's internal service time; the first request in each series carries the TLS handshake, 86 ms and 71 ms.

3 — Public object headers. curl -sI https://miscsubjects.com/img/up/cloudflare-os-r2-hero-card.png returned HTTP 200, content-length: 51205, content-type: image/png, cache-control: public, max-age=31536000, cf-cache-status: MISS. Full body fetch, 189 ms.

4 — A revision read out of R2. curl -s "https://miscsubjects.com/api/articles/cloudflare-os-r2?rev=0" returned rev: 0, is_head: false, prev_hash: "genesis", hash: 24915ae889f6184140a56e89d476d28a31e0cf0e28d31c0990c2aab6f7a626fc, body length 2,878 — out of revisions/cloudflare-os-r2/0.json, not the database. Account identifiers are omitted throughout; the S3 endpoint appears as <ACCOUNT_ID>.r2.cloudflarestorage.com.

The index for the rest of this stack is the Cloudflare account as an operating system.

$18.45
R2 total for the worked 1 TB stored and 10 TB served month
$923.00
S3 total for the same worked month in us-east-1
2.475B
paid Class B calls needed to equal the worked S3 egress line
<0.5
reads per month where a 1 GB object favors Infrequent Access
41,809
objects in the measured production bucket
3.64 GiB
bytes in the measured production bucket
It's not mentioned, but important to note, that R2 lacks object versioning.— kevlened, Hacker News
References (27)
  1. Cloudflare R2 pricing — Cloudflare, Jul 26, 2026“There are no charges for egress bandwidth for any storage class.”
  2. How R2 works — Cloudflare, Jul 26, 2026“Cloudflare R2 is an S3-compatible object storage service with no egress fees, built on Cloudflare's global network.”
  3. Workers R2 API reference — Cloudflare, Jul 26, 2026“R2 writes are strongly consistent. Once the Promise resolves, all subsequent read operations will see this key value pair globally.”
  4. S3 API compatibility — Cloudflare, Jul 26, 2026“When using the S3 API, the region for an R2 bucket is auto”
  5. R2 storage classes — Cloudflare, Jul 26, 2026“Infrequent Access storage is ideal for data that is accessed less frequently.”
  6. R2 object lifecycles — Cloudflare, Jul 26, 2026“Object lifecycles determine the retention period of objects uploaded to your bucket and allow you to specify when objects should transition from Standard storage to Infrequent Access storage.”
  7. R2 bucket locks — Cloudflare, Jul 26, 2026“Bucket locks prevent the deletion and overwriting of objects in an R2 bucket for a specified period — or indefinitely.”
  8. R2 public buckets — Cloudflare, Jul 26, 2026“Expose your bucket using a Cloudflare-managed https://r2.dev subdomain for non-production use cases.”
  9. R2 presigned URLs — Cloudflare, Jul 26, 2026“Presigned URLs are generated client-side with no communication with R2, requiring only your R2 API credentials and an implementation of the AWS Signature Version 4 signing algorithm.”
  10. R2 limits — Cloudflare, Jul 26, 2026“5 GiB (single-part) / 4.995 TiB (multi-part)”
  11. Workers KV limits — Cloudflare, Jul 26, 2026“25 MiB”
  12. D1 limits — Cloudflare, Jul 26, 2026“2,000,000 bytes (2 MB)”
  13. Durable Objects limits — Cloudflare, Jul 26, 2026“Each SQLite-backed Durable Object has a storage limit of 10 GB on a Workers Paid plan.”
  14. Cloudflare Developer Platform terms — Cloudflare, Jul 26, 2026“Cloudflare may temporarily limit your storage and/or the number of requests you can make or receive using the Developer Platform if processing such requests would put an undue burden on the Cloudflare network”
  15. AWS S3 us-east-1 Price List API — Amazon Web Services, Jul 26, 2026“$0.023 per GB - first 50 TB / month of storage used”
  16. Clarify presigned URL docs — GitHub — cloudflare/cloudflare-docs, Jan 22, 2025“I did notice that after reading through the entire documentation and those linked pages.”
  17. Unable to put an object with a key containing three dots to R2 — GitHub — cloudflare/workers-sdk, Jun 25, 2023“Failed to fetch /accounts/*/r2/buckets/*/objects/assets/_...slug_.abcd1234.css - 403: Forbidden”
  18. Cloud object storage benchmark — AIMultiple, Jun 16, 2026“All tests ran from Europe against each provider’s EU-region endpoint.”
  19. The next chapter for Cloudflare Workers: open-source — Hacker News, May 9, 2022“So, with R2 you get unlimited egress, 500x the free limit on requests and flat global pricing. Oh, and the actual storage is cheaper.”
  20. How Canva saves Amazon S3 costs — Hacker News, Jun 19, 2023“See the Docker x Cloudflare case study where improving cache-hit ratio by 2% (by moving to R2) decreased S3 egress fee by 66%”
  21. WordPress Plugin Mirror Downloader (Proof of Concept) — Hacker News, Oct 6, 2024“All up, it would be US$5.35/month starting costs which will rise as number of WordPress plugins and Worker usage increases as outlined here. Cloudflare CDN bandwidth is free of charge, so no need to worry about egress bandwidth fees.”
  22. Ask HN: S3(AWS) vs R2(CF)–Which is better? — Hacker News, Apr 9, 2026“R2's zero egress pricing is genuinely compelling for anything serving large files publicly - media, assets, user uploads. If your use case is "store stuff and serve it to users," R2 wins on cost.”
  23. Comparing AWS S3 with Cloudflare R2: Price, Performance and User Experience — Hacker News, Nov 27, 2024“It's not mentioned, but important to note, that R2 lacks object versioning.”
  24. Hetzner continues its growth in the US with a new location — Hacker News, Dec 5, 2022“They claim $0 egress fees, but their free "Class A operations (mutate state)" and "Class B operations (read state)" have a mothly cap. After that you pay by the number. Isn't that an egress free?”
  25. Comparing AWS S3 with Cloudflare R2: Price, Performance and User Experience — Hacker News, Nov 27, 2024“Is R2 egress actually free, or is it like CFs CDN egress which is "free" until they arbitrarily decide you're using it too much or using it for the wrong things so now you have to pay $undisclosed per GB?”
  26. Cloudflare R2 let me serve almost twice as much data this month as the SEC for $10.80 — Reddit, Jan 19, 2026“I maintain an archive of every SEC filing, accessible via api. I store each filing in R2, compressed with zstandard. I cache egress.”
  27. Live R2 object headers — miscsubjects.com, Jul 26, 2026“cache-control: public, max-age=31536000”