Table of Contents
🌏 中文版
If your app runs on Cloudflare Workers and you need somewhere to store images, S3 works — but you have to manage an AWS account, configure CORS, and keep an eye on egress fees (AWS charges for data transferred out of S3, and those bills can get nasty at scale). R2 solves all of this: S3-compatible API, zero egress fees, running on the Cloudflare network.
What is R2
R2 is Cloudflare's object storage service, designed as a drop-in replacement for S3. Within the Cloudflare Workers ecosystem, R2 is the most natural storage choice.
Key differences from S3
| AWS S3 | Cloudflare R2 | |
|---|---|---|
| API compatibility | Native | S3-compatible (drop-in replacement) |
| Egress (transfer to the Internet) | Billed per GB | Free |
| CDN integration | Requires separate CloudFront setup | Direct Cloudflare CDN |
| Workers integration | Requires SDK + added latency | Native binding, low latency |
| Region choice | Many regions | Automatic placement, plus location hints and jurisdictional restrictions |
Unit prices move — check R2 Pricing and AWS's own page. What stays true is the shape: R2 bills on three axes — storage (GB-month), Class A operations (state-mutating ones like PutObject and ListObjects), and Class B operations (reads like GetObject and HeadObject). Deletes are free operations. Egress is never charged.
Zero egress fees matter a lot for media-heavy applications. Climbing logs involve large numbers of photos and video thumbnails — if those lived in S3, every time someone opened a photo you'd be paying egress. R2 eliminates that cost.
One trap that is easy to underestimate: Class A operations cost an order of magnitude more than Class B. Uploading large numbers of small files, or paging through a bucket with frequent ListObjects calls, produces a bill dominated by operations rather than storage. The Infrequent Access storage class is also not a free win — on top of pricier operations it adds a data retrieval fee and a 30-day minimum storage duration, so cold data does not automatically get cheaper by moving there.
Basic Usage (Cloudflare Workers)
Wrangler configuration binding
{
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "nobodyclimb-media" }]
}
Working with R2 inside a Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1); // /images/foo.jpg → images/foo.jpg
if (request.method === 'PUT') {
await env.BUCKET.put(key, request.body, {
httpMetadata: {
contentType: request.headers.get('content-type') ?? 'application/octet-stream',
},
});
return new Response('Uploaded', { status: 200 });
}
if (request.method === 'GET') {
const object = await env.BUCKET.get(key);
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: {
'content-type': object.httpMetadata?.contentType ?? 'application/octet-stream',
'cache-control': 'public, max-age=31536000', // cache images for 1 year
},
});
}
if (request.method === 'DELETE') {
await env.BUCKET.delete(key);
return new Response('Deleted', { status: 200 });
}
return new Response('Method Not Allowed', { status: 405 });
},
};
Accessing R2 via the S3-Compatible API
R2 also supports the AWS SDK, making it easy to upload from a Next.js server action or any other external service:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
},
});
const command = new PutObjectCommand({
Bucket: 'nobodyclimb-media',
Key: `climbs/${climbId}/photo.jpg`,
Body: imageBuffer,
ContentType: 'image/jpeg',
});
await s3.send(command);
How NobodyClimb Uses R2
NobodyClimb runs on a fully Cloudflare-native stack, with R2 handling all media storage:
User uploads a photo (climb log, story cover image)
│
▼
Hono API (auth + generate upload key)
│
▼
R2 Bucket
nobodyclimb-media/
├── climbs/{climbId}/
│ ├── photo-original.jpg
│ └── photo-thumb.jpg ← thumbnail, video preview
└── stories/{storyId}/
└── cover.jpg
│
▼
Cloudflare CDN (images served with cache-control, cached globally)
Both the original image and a thumbnail are stored separately. Thumbnails are what get displayed during page load — they need to be fast. The original only loads when someone actually opens it, so a bit more latency is acceptable.
Trade-offs
Pros
- Zero egress fees — significant savings for media-heavy applications
- S3-compatible, so migration costs are low
- Native Workers binding with low latency
- Automatic Cloudflare CDN integration
Cons
- Less mature ecosystem than S3 — a decade-plus of third-party tooling, audit and compliance integrations is not caught up quickly
- Fewer selectable physical locations than S3 has regions (R2 places buckets automatically by default; location hints and jurisdictional restrictions exist but with different granularity)
- Large organizations may still need the deep integration that the AWS ecosystem provides
Two things are no longer cons: object lifecycle rules (auto-expiry, auto-transition to Infrequent Access) and event notifications (fire a Queue or Worker on object create/delete — the equivalent of S3's Lambda triggers) both exist now.
When to Choose R2
- You've already committed to Cloudflare Workers as your compute platform
- Your app is media-heavy (images, video thumbnails) and egress costs are a concern
- You're at a medium scale and don't need enterprise-grade AWS features
If you're not using Workers, S3 is probably the better fit. R2's biggest value comes from seamless integration with the Cloudflare ecosystem — using it in isolation reduces the advantage considerably.
Changelog
- 2026-08-19: Fact-checked against primary sources and refreshed; perishable details handed back to official docs. Added to the "Cloudflare Edge Stack" series.
References
- Cloudflare R2 Official Docs
- R2 Pricing — storage, Class A / Class B operations, free tier
- R2 object lifecycle rules
- R2 event notifications
- R2 data location
- Workers Storage Options Guide
- NobodyClimb System Architecture
- Cloudflare KV: Global Edge Key-Value Store
Loading...