Pricing and free tier details in this article are based on publicly available information as of 16/06/2026. Verify against the official pricing pages before making a decision — both Cloudflare and AWS adjust pricing over time.
Quick verdict
- R2 wins clearly for bandwidth-heavy workloads (serving files, media, downloads) — zero egress fees is the decisive advantage
- S3 wins when you need deep integration in the AWS ecosystem: Lambda triggers, EventBridge, Athena, EMR, Step Functions...
- R2's free tier is significantly better and never expires — S3 free tier only lasts 12 months for new accounts
- Migrating from S3 to R2 is straightforward because R2 is S3-API compatible — just swap the endpoint
Quick comparison table
| Cloudflare R2 | AWS S3 (us-east-1) | |
|---|---|---|
| Storage | $0.015/GB/month | $0.023/GB/month |
| Egress to Internet | $0 — free | $0.09/GB (first 10TB) |
| Class A (write, list) | $4.50/million requests | $0.005/1,000 requests |
| Class B (read, GET) | $0.36/million requests | $0.0004/1,000 requests |
| Free tier storage | 10GB/month (never expires) | 5GB/month (first 12 months) |
| Free tier requests | 1M write + 10M read/month | 2,000 PUT + 20,000 GET/month |
| S3-compatible API | Yes | — (native) |
| Built-in CDN | Cloudflare global network | Requires CloudFront (extra cost) |
Egress fees — the reason R2 exists
AWS S3 charges $0.09/GB for every gigabyte transferred out to the Internet. For workloads that serve media, file downloads, or static assets, this is the biggest cost driver — not storage.
Real-world example:
An app with 100GB of data, serving 1TB of downloads per month:
| R2 | S3 | |
|---|---|---|
| Storage | $1.50 | $2.30 |
| Egress (1TB) | $0 | $92.16 |
| Requests (estimated) | ~$1 | ~$1 |
| Monthly total | ~$2.50 | ~$95 |
About 38× difference — just because of egress. The gap widens as traffic increases.
Cloudflare R2 doesn't charge for egress because R2 sits on Cloudflare's own network — bandwidth is already accounted for in their infrastructure. This is exactly why R2 was built: to compete directly with AWS's egress pricing model.
Free tier in detail
Cloudflare R2 free tier
- 10GB storage/month — no expiry, no credit card required to use the free tier
- 1 million Class A operations/month (PUT, POST, COPY, LIST)
- 10 million Class B operations/month (GET, HEAD)
- Egress is always free, even beyond the free tier
Enough to run a small app or side project at zero cost.
AWS S3 free tier
- 5GB storage/month — only applies for the first 12 months after account creation
- 20,000 GET requests/month
- 2,000 PUT requests/month
- Egress is not included — downloads are billed even within the free tier
After 12 months, there's no more free tier. For long-running projects, S3 will have a cost even with small data volumes if there's any outbound bandwidth.
Features — S3 is still more complete
What R2 has
- Core object storage (PUT, GET, DELETE, LIST)
- Object versioning
- Lifecycle rules (auto-delete objects after N days)
- Presigned URLs
- CORS configuration
- Custom domains via Cloudflare
- Event notifications via Cloudflare Queues
- S3-compatible API (use the AWS SDK, just swap the endpoint)
What S3 has that R2 doesn't
- Direct Lambda triggers on new objects — R2 requires routing through Queues + Workers
- S3 Select — query data inside CSV/JSON/Parquet files without downloading them
- S3 Inventory — scheduled reports listing all objects in a bucket
- Storage Classes: Standard, Infrequent Access, Glacier — significant savings for cold data
- Cross-region and cross-account replication
- Native integration with dozens of AWS services: Athena, EMR, Redshift Spectrum, Glue, Step Functions, EventBridge...
- Access Points — granular access control by VPC, prefix, or account
- More regions (30+)
R2 is the right fit for storing and serving files. S3 is the right fit when storage is one link in a larger data pipeline.
Migrating from S3 to R2
R2's S3 API compatibility means migration usually only requires swapping the endpoint and credentials:
// Before: AWS S3
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
// After: Cloudflare R2 — only the endpoint and credentials change
const r2 = new S3Client({
region: "auto",
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
All operations — PutObjectCommand, GetObjectCommand, DeleteObjectCommand — stay identical. No changes to business logic.
Migration notes:
- Buckets must be recreated on R2 (no auto-sync)
- Objects need to be copied manually or with
rclone - If you're using S3 event triggers into Lambda, refactor to Cloudflare Queues + Workers
- Presigned URL format changes endpoint but the generation logic is the same
Syncing data from S3 to R2 with rclone
# Configure R2 in rclone
rclone config create r2 s3 \
provider Cloudflare \
access_key_id YOUR_R2_KEY \
secret_access_key YOUR_R2_SECRET \
endpoint https://ACCOUNT_ID.r2.cloudflarestorage.com \
acl private
# Copy entire bucket from S3 to R2
rclone copy s3:your-s3-bucket r2:your-r2-bucket --progress
R2 or S3 — which to choose
Choose R2 when:
- App serves files, images, or video to users — save on egress
- Side project or early startup keeping costs low
- No need for deep AWS service integration
- Want a free tier with no expiry
Choose S3 when:
- System already runs on AWS and needs Lambda, Athena, EMR, or EventBridge integration
- Need S3 Select to query data inside files directly
- Require cross-region replication with specific SLA guarantees
- Team is already familiar with AWS IAM and doesn't want another vendor
Use both when:
- Store cold data or long-term backups on S3 Glacier (cheaper per GB)
- Serve hot data, media, and assets via R2 (zero egress cost)