When you're picking object storage for a production app — static assets, user uploads, backups, build artefacts — the choice usually comes down to two services: Amazon S3, the de facto standard, and Cloudflare R2, the S3-compatible challenger that ships zero egress fees. The right pick rarely turns on storage cost per GB. It turns on egress economics, latency to your users, API compatibility with the SDK you're already using, and how the bill scales as traffic grows.
This is a practitioner-led comparison: pricing as it stands today, real performance characteristics, the migration pitfalls most posts skip, and where each service genuinely fits. If you're running a single static site, deploying multi-environment SaaS, or building an agency deployment workflow across dozens of client projects, the trade-offs hit differently — we'll flag those.
TL;DR — which one wins
- Pick R2 if you serve significant read traffic to the public internet, especially media, downloads, or globally-distributed sites. The egress savings dominate the bill at any non-trivial scale.
- Pick S3 if you're deep in the AWS ecosystem (Lambda, RDS, EMR, Athena, Glacier), need the broadest set of storage classes, or your workload is intra-AWS (where egress to other AWS services in-region is free anyway).
- Pick both if you're cost-aware: keep cold archives or AWS-internal workloads in S3, push hot public assets to R2 in front of a CDN. This is a common pattern and easier than people think.
Pricing — the only table that matters
Pricing for both services moves; figures below are list prices as of May 2026. Always re-check the Cloudflare R2 pricing page and AWS S3 pricing page before committing.
| Cost component | AWS S3 Standard (us-east-1) | Cloudflare R2 (Standard) |
|---|---|---|
| Storage (per GB/month) | $0.023 (first 50 TB) | $0.015 |
| PUT / COPY / POST / LIST (per million) | $5.00 | $4.50 (Class A) |
| GET / SELECT / HEAD (per million) | $0.40 | $0.36 (Class B) |
| Egress to internet (per GB) | $0.09 (first 10 TB) | $0 — free |
| Free tier | 5 GB / 20K GET / 2K PUT (12 mo) | 10 GB / 10M GET / 1M PUT (forever) |
| Infrequent Access tier | S3 IA $0.0125/GB + retrieval | R2 IA $0.01/GB + $0.01/GB retrieval |
The egress row is the whole story. A 1 TB media library serving 5 TB/month of public reads costs roughly:
- S3 Standard: $23 storage + ~$450 egress (after the 100 GB free tier) = ~$473/month
- R2 Standard: $15 storage + $0 egress = ~$15/month
That's a 30× swing, and it's why R2 gets so much attention for any workload that fans out to the public web.
Egress isn't the only fee that scales
R2 wins on egress but loses any pretence at free
the moment your write path is hot. If you're streaming write-heavy workloads — say, video chunks, log shipping, or analytics events — Class A operations dominate. At 100 M PUTs/month you're paying $450 on R2 vs $500 on S3. The savings shrink to single-digit percentages. Always model the actual op mix; don't assume R2 is cheaper end-to-end.
S3's egress also has a quietly important escape hatch: transfers to other AWS services in the same region are free. If your app is EC2/ECS/Lambda → S3 → user-via-CloudFront, the only egress you pay is at the CloudFront edge, and CloudFront-to-S3 is free. Many teams over-estimate S3 egress because they forget this.
Performance — global edge vs regional buckets
R2's design assumption: every object should be deliverable from any of Cloudflare's 300+ POPs without provisioning anything. There's no concept of region selection at the bucket level (you can hint a jurisdiction for data residency). Latency for first-byte reads in production typically sits at 40–80 ms globally, and Cloudflare's CDN sits directly in front of R2 with zero configuration.
S3 buckets are regional. A bucket in us-east-1 serves European users at 100–150 ms first-byte until you put CloudFront in front of it. With CloudFront tuned, S3 + CloudFront is just as fast as R2 + Cloudflare — but you're paying CloudFront request fees and (more importantly) configuring two services instead of one. For greenfield projects with no AWS gravity, R2's bucket-is-already-on-the-edge
model removes a real chunk of operational work.
For PUT throughput, S3 is the more mature platform. Multipart uploads, Transfer Acceleration, and S3 Express One Zone (single-digit-millisecond latency for hot data) have no direct R2 equivalent. If you're ingesting at gigabit rates, S3 still wins.
S3 API compatibility — close, not identical
R2 advertises S3 API compatibility, and for 90% of SDK calls it just works. The gotchas that bite people in production:
- No object versioning until recently — R2 added versioning in 2024 but it lags S3's feature depth. If you rely on lifecycle rules that pin specific noncurrent versions, validate carefully.
- Bucket policies are limited — R2 uses Cloudflare API tokens scoped to buckets rather than IAM JSON policies. If your security model leans on fine-grained IAM (resource ARNs, conditions, principals), the translation is non-trivial.
- Presigned URLs work, but TTL semantics differ slightly — most SDKs handle this transparently, but if you've built your own signing, expect to test.
- No SSE-KMS — R2 does encryption at rest with Cloudflare-managed keys. There's no equivalent to S3's customer-managed KMS keys. For workloads under compliance regimes (HIPAA, FedRAMP) requiring CMK, this is a hard blocker.
- No requester-pays buckets — useful for distributing large open datasets where you want consumers to foot the bandwidth bill.
- Event notifications differ — S3 events fan out to SNS/SQS/Lambda. R2 events go via Cloudflare Workers or Event Notifications to queues. Different SDK, different latency profile.
For the vast majority of use cases — static assets, user uploads, build artefacts, deploy targets — none of these matter. For regulated or AWS-integrated workloads, audit your dependency on IAM policies and KMS before migrating.
Storage class depth — S3 is still the only adult in the room
S3 ships seven storage classes (Standard, Intelligent-Tiering, Standard-IA, One Zone-IA, Glacier Instant Retrieval, Glacier Flexible Retrieval, Glacier Deep Archive). For lifecycle-managed archives where you tier 2-year-old logs to Glacier Deep Archive at $0.00099/GB/month, S3 is unmatched. R2 has exactly two: Standard and Infrequent Access. If your archival strategy depends on multiple cold tiers, R2 alone won't replace S3.
Migration — how to actually move data
The least talked-about reality of R2 migration: the bandwidth bill belongs to S3. Egressing 10 TB out of S3 to R2 costs ~$900 in S3 egress fees alone. Three approaches:
- R2 Super Slurper — Cloudflare's managed migration tool that pulls objects from S3 (or any S3-compatible source) into R2. Handles auth, pagination, and retries. Free to use, but you still pay S3 egress.
- Sippy (R2 incremental migration) — R2 transparently pulls objects from your S3 bucket on first read, then serves from R2 thereafter. Useful when you can't afford a hard cutover. Slow path on cache miss; great for long-tail content.
rclonefrom a cheap egress region —rclone sync s3:bucket r2:bucketwith parallelism tuning. Works fine for small migrations. For >1 TB use Super Slurper.
If you control both ends, do a final sync with a short application freeze, flip DNS / config, and keep S3 as a read-only fallback for 30 days. Don't decommission until you've verified the long tail.
Where DeployHQ fits
Object storage isn't a deployment target on its own — but if you're shipping static sites, SPA bundles, or asset pipelines, the storage bucket is usually the last hop. DeployHQ deploys directly to both S3 and R2 from your Git repository, runs your build pipeline (esbuild, Vite, Webpack, whatever), and pushes the artefacts on every push to your release branch. Concretely, you can:
- Deploy from a GitHub repository directly to an S3 bucket — same flow works for Bitbucket and GitLab.
- Push static builds to R2 with zero downtime deployments so users never see a half-deployed site.
- Wire in custom S3 request headers for cache-control, CORS, and content-type overrides as part of the deploy.
- Trigger a Cloudflare cache purge automatically after each deployment so your edge serves the new build immediately.
- For backups, see our walkthrough on implementing server backups to AWS S3 using the same credentials.
If you're running multi-environment deployments across staging and production for an agency, you get isolated buckets per environment with one-click rollback on the CDN layer to recover from a bad release.
Decision matrix — pick by workload, not by hype
| Your workload | Pick |
|---|---|
| Public-facing media, downloads, large static sites | R2 |
| Deep AWS stack (Lambda, RDS, Athena, Glacier) | S3 |
| Multi-region archives with lifecycle tiering | S3 |
| Greenfield SaaS, no AWS gravity | R2 |
| Compliance requiring SSE-KMS / customer-managed keys | S3 |
| High-write log/event ingest at GB/s | S3 |
| Free-tier-friendly side projects | R2 (10 GB free forever) |
| Hybrid: cold archive in S3, hot reads from R2 | Both |
Final word
S3 vs R2 isn't a winner-takes-all contest. R2 is the better default for new projects that serve significant public read traffic. S3 is the better default for anything intra-AWS, regulated, or dependent on cold-storage tiering. The bill that matters is your bill — model both with your actual op mix and egress profile, run a 30-day pilot if you can, and don't migrate just because the egress headline looks good.
Once you've picked, the next step is wiring up a deployment pipeline that pushes builds automatically on every push. DeployHQ deploys to both — sign up for a free DeployHQ account and have your first S3 or R2 deployment running in under ten minutes.
Questions or want to chat through a specific migration scenario? Reach out at support@deployhq.com or find us on X / Twitter.