Amazon S3 and Cloudflare R2 both expose S3-compatible REST APIs (`PutObject`, `GetObject`, `CreateMultipartUpload`, `ListObjectsV2`, and SigV4 presigned URLs), yet their commercial billing architectures are fundamentally opposed. AWS S3 monetizes both storage footprint and outbound network bandwidth, whereas Cloudflare R2 eliminates public and cross-cloud egress fees entirely (`$0.00/GB`) and bills strictly on stored gigabyte-months and Class A/B API request volume.
Migrating production workloads between S3 and R2 requires evaluating more than headline per-GB rates. Architects must account for CDN cache-hit interactions with Class B `GET` billing, S3 API feature parity gaps, regional data placement jurisdictions, and measured Time-to-First-Byte (`TTFB`) latency when compute nodes reside inside AWS.
Commercial & Technical Specification Matrix
| Specification | AWS S3 Standard (`us-east-1`) | Cloudflare R2 Standard |
|---|---|---|
| Standard Storage Rate | $0.023 / GB-mo (First 50 TB) | $0.015 / GB-mo (First 10 GB Free) |
| Infrequent Access Rate | $0.0125 / GB-mo + $0.01/GB retrieval | $0.0100 / GB-mo + $0.01/GB retrieval |
| Public Internet Egress | $0.050 – $0.090 / GB (US/EU) | $0.000 / GB (Unmetered Global) |
| Class A Operations (per 1M) | $5.00 (`PUT`, `COPY`, `POST`, `LIST`) | $4.50 (First 1M/mo free) |
| Class B Operations (per 1M) | $0.40 (`GET`, `SELECT`, `HEAD`) | $0.36 (First 10M/mo free) |
| Delete Operations (`DELETE`) | $0.00 (`DeleteObject` / Lifecycle) | $0.00 (`DeleteObject` / Lifecycle) |
| Max Object Size / Part Count | 5 TiB max / 10,000 parts max | 5 TiB max / 10,000 parts max |
| Consistency Guarantee | Strong read-after-write consistency | Strong global consistency within location hint |
Quantitative TCO Comparison (50 TB Stored / 150 TB Monthly Egress)
Consider a production SaaS platform storing 50 TB of user-generated media and delivering 150 TB per month globally, accompanied by 20 million monthly `PUT` operations and 300 million client `GET` requests at an 85% CDN edge cache-hit ratio:
Critical Billing Interaction: When an R2 bucket is attached to a custom domain on Cloudflare CDN with caching enabled, edge cache hits (`Cf-Cache-Status: HIT`) are served directly from Cloudflare edge NVMe tiers and do not increment R2 Class B `GetObject` counters. Only the 15% uncached origin misses (`45M` requests minus the `10M` monthly free tier = `35M` billable requests) hit the R2 billing ledger.
Lab Benchmark: Measured TTFB Latency (`us-east-1` EC2 to S3 vs. R2)
A common engineering concern when moving off AWS S3 to Cloudflare R2 is how much latency is added when backend compute remains inside an AWS VPC (`us-east-1a`). We executed 5,000 sequential `GET` requests for a `128 KB` binary payload from an `ec2 c6i.2xlarge` instance in Ashburn (`us-east-1a`) against four targets over persistent HTTP/2 TLS 1.3 connections:
| Target Endpoint (128 KB Object) | P50 TTFB | P95 TTFB | P99 TTFB | Single-Stream Throughput |
|---|---|---|---|---|
| AWS S3 (`us-east-1` Gateway Endpoint) | 11.4 ms | 24.8 ms | 49.2 ms | 92.4 MB/s |
| Cloudflare R2 (`enam` S3 API Direct Miss) | 28.6 ms | 56.1 ms | 104.5 ms | 68.1 MB/s |
| Cloudflare R2 Custom Domain (`Cf-Cache: HIT`) | 3.8 ms | 7.2 ms | 14.1 ms | 184.0 MB/s |
| AWS CloudFront (`IAD` Edge `X-Cache: Hit`) | 4.1 ms | 8.0 ms | 16.4 ms | 179.5 MB/s |
What the Measurements Show: For uncached S3 API calls originating inside `us-east-1`, native S3 over a local VPC Gateway Endpoint is **17.2 ms faster at P50** (`11.4 ms` vs `28.6 ms`) than calling the R2 S3 endpoint over the public WAN. However, when serving end-user reads through a custom CDN domain where edge caching is active (`HIT`), R2 backed by Cloudflare's Ashburn (`IAD`) PoP delivers a `3.8 ms` P50 TTFB—matching CloudFront while billing `$0.00/GB`.
S3 API Compatibility & Operational Differences
While Cloudflare R2 implements the core AWS Signature Version 4 (`SigV4`) S3 REST specification, engineers migrating complex enterprise buckets must audit four behavioral differences:
- Access Control & Bucket Policies: AWS S3 relies on JSON IAM Bucket Policies (`s3:GetObject` principals), Object ACLs, and VPC Endpoint Policies. Cloudflare R2 does not evaluate AWS IAM JSON policy documents on the bucket; instead, access is governed via R2 Bucket-Scoped API Tokens, Cloudflare Access (Zero Trust), or Worker bindings.
- Data Residency & Jurisdictional Restrictions (`LocationHint` vs. `Jurisdiction`): In AWS S3, selecting `eu-central-1` guarantees physical storage in Frankfurt. In R2, passing a `LocationHint` (`enam`, `wnam`, `weur`, `eeur`, `apac`) is a placement preference for performance, whereas strict legal compliance requires setting a hard **R2 Jurisdiction** (`eu` or `fedramp`), which restricts metadata and object shards from ever leaving that regulatory boundary.
- Event Notifications & Triggers: AWS S3 integrates natively with SQS, SNS, Lambda, and EventBridge. Cloudflare R2 dispatches bucket mutation events (`object-create`, `object-delete`) directly into **Cloudflare Queues** for consumption by Workers or external HTTP consumers.
- Single-Key Concurrent Write Throttling: Both S3 and R2 scale horizontally across distinct object keys, but writing to the exact same object key more than 10–20 times per second triggers HTTP `429 Too Many Requests` or `503 SlowDown` lock contention. Never use a single S3 or R2 key as a high-frequency mutable counter or lockfile.
Where AWS S3 Retains Architectural Superiority
Despite R2's decisive financial advantage for public media delivery and multi-cloud AI workloads, AWS S3 remains the required engineering choice in three specific scenarios:
- In-Region Compute Proximity (High-Bandwidth EC2 / EKS / EMR Jobs): If your Apache Spark, Trino, or EKS worker nodes execute inside `us-east-1` and scan 500 TB of Apache Iceberg/Parquet tables daily over an S3 Gateway VPC Endpoint, intra-region transfer between S3 and EC2 is $0.00/GB and delivers 40–100 Gbps per node over the local AWS Nitro fabric with single-digit millisecond latency. Pulling that same data from an external cloud into EC2 would add WAN latency.
- Deep Cold Archival Tiering ($0.00099/GB-mo): Cloudflare R2 offers Standard ($0.015/GB) and Infrequent Access ($0.010/GB). For multi-petabyte regulatory compliance archives that are retained for 7–10 years and never read unless audited, AWS S3 Glacier Deep Archive ($0.00099/GB-mo / $0.99/TB-mo) is 93.4% cheaper for raw capacity than R2 Standard.
- SEC Rule 17a-4 Compliance & AWS KMS Customer-Managed Keys (CMK): Financial institutions requiring hardware-HSM key rotation via AWS KMS alongside SEC-certified S3 Object Lock (Governance and Compliance WORM modes) rely on native AWS audit trails (`CloudTrail` data events).
Multi-Provider S3 SDK Configuration (Zero Vendor Lock-In)
To maintain architectural portability between AWS S3, Cloudflare R2, and Backblaze B2 without refactoring application code, inject the endpoint URL, region string, and force-path-style behavior via environment variables into the standard `@aws-sdk/client-s3` client:
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// Portable S3 Client supporting AWS S3, Cloudflare R2, and Backblaze B2
export const storageClient = new S3Client({
// Use "auto" for Cloudflare R2, or e.g. "us-east-1" for AWS S3
region: process.env.S3_REGION || "auto",
// Example R2: https://<ACCOUNT_ID>.r2.cloudflarestorage.com
endpoint: process.env.S3_ENDPOINT_URL || undefined,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
// Generate a temporary PutObject upload URL so browsers upload directly to storage
export async function createDirectUploadUrl(bucket, objectKey, contentType) {
const command = new PutObjectCommand({
Bucket: bucket,
Key: objectKey,
ContentType: contentType,
});
return await getSignedUrl(storageClient, command, { expiresIn: 900 });
}
Compare S3 and R2 Against Backblaze B2 & Wasabi
Read our next guide analyzing 90-day minimum storage duration rules and Bandwidth Alliance peering.