You've built the perfect React app slick UI, fast interactions, maybe even some fancy image uploads for user profiles or a Next.js blog with tons of media. Everything's humming along locally... until you deploy and the storage bills hit. AWS S3 egress fees eat your budget the moment users start downloading their own avatars. Sound familiar?
That's the pain Cloudflare R2 solves. No egress charges. S3-compatible API. Runs on Cloudflare's massive global network. But how does it actually pull this off without turning into a consistency nightmare or a latency slug?
In this post, you'll get a clear, no-fluff tour of R2's internal architecture. We'll go from high-level concepts to the clever tricks that make it feel magical for frontend workβcomplete with code you can steal for your next side project. By the end, you'll know exactly when (and how) to reach for R2 instead of S3, Backblaze, or even local storage hacks.
Table of Contents
- The Core Promise: Zero Egress, Global Scale
- R2's High-Level Architecture Overview
- Metadata Magic: Durable Objects Keep It Consistent
- Reading Data: Tiered Cache + Global Distribution
- Writing Data: Local Uploads and Background Replication
- Practical Example: Uploading and Serving Images from a Next.js App
- Advanced Tips and Gotchas for Frontend Devs
- Common Mistakes That Burn You
- Wrapping Up: Level Up Your Storage Game
The Core Promise: Zero Egress, Global Scale
Why care about internals? Because understanding why R2 behaves the way it does helps you design better systems.
Traditional object stores like S3 charge for data leaving their region. Cloudflare's edge network (330+ locations) changes the game: data is served from the nearest point-of-presence (PoP), often without ever hitting a "central" origin. No egress bill because the transfer happens on Cloudflare's backbone.
But global-anywhere reads + strong consistency sounds impossible. R2 achieves it by decoupling metadata from payload storage and leaning hard on Cloudflare primitives.
Always think "edge-first." If your app serves public assets (images, videos, user uploads), R2 + Cloudflare Images/Stream can slash costs and latency compared to centralized providers.
R2's High-Level Architecture Overview
R2 breaks into four key layers:
- R2 Gateway β Edge Workers handling auth, routing, S3 API translation.
- Metadata Service β Distributed Durable Objects for object keys, checksums, versionsβensuring strong consistency.
- Tiered Read Cache β Multi-level caching leveraging Cloudflare's Tiered Cache.
- Distributed Storage Infrastructure β Encrypted, erasure-coded persistent storage across regions.
Requests hit the nearest edge β gateway routes β metadata checked β data pulled from cache or backing store.
Metadata Magic: Durable Objects Keep It Consistent
Durable Objects are single-instance, stateful Workers. R2 uses them for metadata: each object key maps to a Durable Object that holds info like ETag, size, last-modified.
When you PUT an object:
- Gateway routes to the DO responsible for that key.
- DO updates metadata atomically.
- Payload goes to storage.
Reads hit the DO first β fast metadata cache β then data fetch.
This avoids distributed locks or consensus hell. Durable Objects provide coordination without traditional databases.
For **custom **needs, bind R2 buckets directly in Workers for zero-latency metadata ops.
Tsx
// In a Cloudflare Worker
export default {
async fetch(request, env) {
const object = await env.MY_BUCKET.get('profile-pic.jpg');
if (object === null) return new Response('Not found', { status: 404 });
const data = await object.arrayBuffer();
return new Response(data, {
headers: { 'Content-Type': 'image/jpeg' },
});
}
};
Reading Data: Tiered Cache + Global Distribution
Reads shine here. Cloudflare Tiered Cache sits in front:
- L1: PoP-level cache (super fast if hit).
- L2: Regional.
- Miss β Distributed Storage.
Because data is replicated/erasure-coded across locations, hot objects get cached globally.
No egress: delivery is from edge cache β client.
Accessibility Note
When serving images, always add alttext and consider loading="lazy" + responsive srcset. R2 pairs perfectly with <Image> components in Next.js.
Writing Data: Local Uploads and Background Replication
New in 2026: Local Uploads (beta). Enable it β client uploads to nearest PoP storage.
Metadata publishes immediately to bucket's "home" region DO. Background job replicates payload asynchronously with retries.
Result: ~75% lower write latency for global users no waiting for cross-region sync.
Without Local Uploads, writes route to bucket's location hint (or auto).
Practical Example: Uploading and Serving Images from a Next.js App
Let's build a simple user avatar uploader.
- Create R2 bucket in dashboard.
- Generate S3-compatible credentials (Access Key ID, Secret).
- Use
@aws-sdk/client-s3with custom endpoint.
Tsx
// lib/r2.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
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!,
},
});
export async function uploadAvatar(file: File, userId: string) {
const key = `avatars/${userId}.jpg`;
await r2.send(new PutObjectCommand({
Bucket: 'my-app-bucket',
Key: key,
Body: Buffer.from(await file.arrayBuffer()),
ContentType: file.type,
}));
return `https://pub-<your-pub-hash>.r2.dev/${key}`;
}
In your component:
Tsx
// app/upload/page.tsx
'use client';
import { uploadAvatar } from '@/lib/r2';
export default function Upload() {
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const url = await uploadAvatar(file, 'krish123');
console.log('Avatar URL:', url);
};
return <input type="file" onChange={handleUpload} />;
}
Common Mistakes That Burn You
- Forgetting region=
autoin SDK β weird errors. - Assuming eventual consistency β test read-after-write.
- Public bucket without CORS β browser blocks fetches.
- Ignoring Class B ops costs β listObjectsV2 can add up on large buckets.
Wrapping Up: Level Up Your Storage Game
R2 isn't just "cheap S3." Its edge-native design, Durable Objects metadata, tiered caching, and new Local Uploads make it a frontend superpower: fast, predictable costs, global performance without ops overhead.

Top comments (0)