R2 vs S3: Why I Don't Use AWS to Store Images
English · 繁中 · Español · 日本語 · Português (BR)
Author: Danko Peng (@dankopeng)
Updated: March 2026
Reading time: ~6 minutes
Does Your App Need to Store Images?
User avatars, product screenshots, blog cover images—any app with media upload needs a place to store these files.
Databases aren't suitable for storing images (databases are for structured text data), you need a dedicated "object storage" service.
The most common choice is AWS S3. But if your app runs on Cloudflare, there's a better option: Cloudflare R2.
S3's Hidden Cost: Egress Fees
AWS S3 pricing has a trap many people fall into: egress fees.
- Store images in S3: storage fee (~$0.023/GB/month)
- Users download images: egress fee (~$0.09/GB)
Doesn't sound like much? Let's calculate:
Say your app has 1,000 users, each downloads an average of 10 images daily, 500KB each:
Daily egress = 1,000 × 10 × 0.5MB = 5,000MB = 5GB
Daily egress cost = 5GB × $0.09 = $0.45
Monthly egress cost = $0.45 × 30 = $13.5The more successful your app, the higher the bill. Some companies only discover S3 egress fees dominate costs after their app goes viral.
R2's Pricing Logic
Cloudflare R2's pricing strategy is completely different:
| Cost Item | AWS S3 | Cloudflare R2 |
|---|---|---|
| Storage | $0.023/GB/month | $0.015/GB/month |
| Egress | $0.09/GB | $0 — Completely free |
| Free tier | Limited | 10GB storage + 1M operations/month |
Zero egress fees make R2's advantage very clear for high-traffic apps. The more your images are downloaded, the bigger the cost difference versus S3.
Technical Differences Between R2 and S3
| AWS S3 | Cloudflare R2 | |
|---|---|---|
| API compatibility | Native S3 API | S3 API compatible |
| Global distribution | Manual multi-region setup | Automatic global distribution |
| Workers integration | Cross-network calls | Direct Binding, no public internet |
| CORS setup | Manual configuration | Handle through Workers, can be zero CORS |
S3 API compatibility is important—most packages supporting S3 (like @aws-sdk/client-s3) can directly operate R2, just swap the endpoint URL. This makes migration costs very low.
Using R2 in Cloudflare Workers
Basic upload and read pattern:
// Upload image to R2
await env.R2.put(`images/${filename}`, fileBuffer, {
httpMetadata: { contentType: file.type }
})
// Return R2 file through Workers
const object = await env.R2.get(`images/${filename}`)
if (!object) return new Response("Not found", { status: 404 })
return new Response(object.body, {
headers: { "Content-Type": object.httpMetadata?.contentType || "application/octet-stream" }
})Proxying R2 access through Workers (rather than exposing public URLs directly) lets you add any logic in Workers: verify user permissions, track download counts, add cache control, etc. With vibefast.app, R2 binding configuration is already handled, just use env.R2 directly in Workers.
When to Consider S3?
R2 is better for Cloudflare ecosystem apps in almost every aspect, but a few situations might still need S3:
- You need S3's advanced features (like Intelligent Tiering, Glacier cold storage)
- Your app isn't on Cloudflare, R2's advantages aren't as obvious
- You need deep integration with existing S3-dependent services
For most indie apps running on Cloudflare, R2 is the more natural choice.
Danko Peng
X · YouTube · Threads
Ready to skip the setup and start building?
👉 vibefast.app — Early bird $99, price increases to $199 on August 1, 2026.