Cloudflare R2: S3-Compatible Object Storage With Zero Egress Fees
AWS S3 charges $0.09/GB for egress. Cloudflare R2 charges $0.00. For apps with heavy file downloads — user uploads, generated PDFs, images — R2 eliminates one of the largest cloud bills at scale.
R2 vs S3 Pricing
| S3 | R2 | |
|---|---|---|
| Storage | $0.023/GB | $0.015/GB |
| Egress | $0.09/GB | $0.00 |
| Class A ops (PUT) | $0.005/1k | $0.0045/1k |
| Class B ops (GET) | $0.0004/1k | $0.00036/1k |
For 1TB egress/month: S3 = $90. R2 = $0.
Setup with AWS SDK (S3-Compatible)
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const r2 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
Upload a File
async function uploadFile(key: string, buffer: Buffer, contentType: string) {
await r2.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: contentType,
}));
return `https://${process.env.R2_PUBLIC_DOMAIN}/${key}`;
}
// Usage in file upload handler
const buffer = Buffer.from(await file.arrayBuffer());
const key = `uploads/${userId}/${Date.now()}-${file.name}`;
const url = await uploadFile(key, buffer, file.type);
Presigned Upload URLs (Direct from Browser)
// Server: generate presigned URL
async function getUploadUrl(key: string, contentType: string) {
const url = await getSignedUrl(
r2,
new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME,
Key: key,
ContentType: contentType,
}),
{ expiresIn: 3600 }
);
return url;
}
// Client: upload directly to R2 (bypasses your server)
const { url, key } = await fetch('/api/upload-url', {
method: 'POST',
body: JSON.stringify({ filename: file.name, type: file.type }),
}).then(r => r.json());
await fetch(url, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
Serving Files via Public Domain
// Set a custom domain in R2 bucket settings
// Then files are available at: https://files.yourdomain.com/key
function getPublicUrl(key: string) {
return `https://${process.env.R2_PUBLIC_DOMAIN}/${key}`;
}
Deleting Files
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
async function deleteFile(key: string) {
await r2.send(new DeleteObjectCommand({
Bucket: process.env.R2_BUCKET_NAME,
Key: key,
}));
}
Migration from S3
R2 is S3-compatible — change the endpoint URL and credentials. No code changes needed beyond that.
Cloudflare R2 storage integration ships in the AI SaaS Starter Kit — presigned uploads, public URL generation, deletion helpers pre-configured. $99 at whoffagents.com.
Top comments (0)