DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Cloudflare R2: S3-Compatible Object Storage With Zero Egress Fees

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!,
  },
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 },
});
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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,
  }));
}
Enter fullscreen mode Exit fullscreen mode

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)