Open Calculator

AWS S3 vs Cloudflare R2: Zero-Egress Object Storage Architecture and Class A/B API Economics

Published March 09, 2026 · Updated September 19, 2026 · Storage Architecture & Latency Benchmarks

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:

// Scenario A: AWS S3 Standard + Amazon CloudFront (85% Cache-Hit Ratio)
S3 Storage (50,000 GB × $0.023): $1,150.00
S3-to-CloudFront Origin Fetch Bandwidth (22.5 TB): $0.00 (Waived by AWS)
CloudFront Edge Egress (150,000 GB × $0.065 blended): $9,750.00
S3 API (20M PUT × $5 + 45M Origin Miss GET × $0.40): $118.00
CloudFront HTTPS Request Surcharge (300M × $1.00/1M): $300.00
Total Monthly AWS Stack Bill: $11,318.00 / mo
// Scenario B: Cloudflare R2 + Cloudflare CDN (85% Cache-Hit Ratio)
R2 Storage (50,000 GB × $0.015): $750.00
R2 Origin & Cloudflare CDN Edge Bandwidth (150,000 GB × $0.00): $0.00
R2 Class A (19M billable × $4.50) + Class B (35M billable × $0.36): $98.10
Cloudflare CDN Cached Edge HTTPS Request Surcharge: $0.00
Total Monthly Cloudflare R2 Bill: $848.10 / mo (92.5% Savings)

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:

  1. 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.
  2. 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.
  3. 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.

B2 vs Wasabi Guide →