Object Storage Explained: S3, R2, and the Modern Way to Store Files
There is a quiet gap between how documentation presents object storage explained: s3, r2, and the modern way to store files and how it behaves under real traffic. This...
There is a quiet gap between how documentation presents object storage explained: s3, r2, and the modern way to store files and how it behaves under real traffic. This article exists to close that gap, with patterns drawn from deployments that have survived production, spikes, and the occasional incident.
Introduction
This guide covers how both work, the pricing models that trip people up — especially the egress trap — and a production pattern for uploads, resizing, and delivery that scales from a hobby project to millions of requests. It is the same pattern this site uses for its images and assets.
Half of the web's performance problems are not about code — they are about where files live and how they travel. Object storage and content delivery networks (CDNs) are the two quiet technologies that decide whether a page feels instant or sluggish, and they are the least glamorous, most consequential infrastructure most applications depend on.
Object storage is the modern way to store files: not in a folder hierarchy on one disk, but as objects in a globally replicated bucket, addressed by a URL and accessed over HTTP. CDNs take the files you already serve and copy them to hundreds of edge locations, so a user in Singapore gets the same image from a server in Kuala Lumpur instead of one in Frankfurt.
The model in practice: the pieces fit together before the first line of application code.
Why It Matters
Object storage removes the hardest part of file handling from your application: durability, replication, and scaling are the provider's problem. A bucket holds a million files as easily as a hundred, and you never defragment, back up, or migrate it by hand. The application just reads and writes URLs.
CDN delivery is the single biggest free performance win available to most websites. Serving static assets from the edge cuts latency by orders of magnitude for international users, reduces origin load to near zero, and — properly configured — improves every metric that matters, from Core Web Vitals to bounce rate.
- Object storage is unlimited in practice: no disk sizes, no partitions, no maintenance
- Durability is the provider's job: 99.999999999% is a different kind of 'backup'
- A CDN turns one origin server into a global network for the price of a few dollars
- Cache-friendly delivery reduces origin bandwidth bills dramatically
- URLs are the interface: upload once, and every service can fetch the same object
The Problem
The problems start when applications treat object storage like a filesystem: hot links, no cache headers, sync loops, and full-download resizing pipelines that hammer the origin. The other classic failure is the billing surprise — storage is cheap, but egress (data leaving the provider) is where the bill explodes, especially for image-heavy sites.
The second failure mode is architectural: building a custom upload, resize, and delivery pipeline with background workers and in-house caching, when the same result is available from a bucket, a function, and a CDN with a day of configuration.
The Approach
The modern pattern has four pieces. Uploads go directly from the client to the bucket via signed URLs — the server never handles the binary, so it never becomes the bottleneck. Processing happens in a triggered function: resize, compress, and derive multiple formats. Storage keeps the original and the derivatives in the same bucket, organized by a key scheme. Delivery happens through a CDN in front, with cache headers that make the edge do the heavy lifting.
The key insight is the key scheme: every asset gets a content-addressed or versioned key, so the CDN can cache forever. Files that are immutable — hashed names, versioned images — get cache headers measured in months, and the edge serves them from memory while the origin barely notices.
The whole pipeline in four steps. The server coordinates; the bucket stores; a function transforms; the CDN delivers. Nobody in this chain downloads a full-size image just to serve a thumbnail, and the origin is never in the hot path of a single request.
// The production asset pipeline in one sketch
// 1. Client asks for a signed upload URL — server never touches the bytes
const url = await bucket.getSignedUploadUrl(`originals/${user.id}/${uuid}.jpg`);
// 2. Client PUTs directly to the bucket
await fetch(url, { method: 'PUT', body: file });
// 3. A triggered function resizes and derives formats
await resize(file, [{ width: 1600, format: 'webp' }, { width: 640, format: 'avif' }]);
// 4. The CDN serves the result from the edge, cached by versioned key
// https://cdn.example.com/derived/1600.webp/abc123 -> immutable, cached for a year
The workflow applied: configuration and discipline, not heroics.
S3 vs R2 vs Traditional File Storage
| Factor | AWS S3 | Cloudflare R2 | Server Disks |
|---|---|---|---|
| Egress fees | Yes (the trap) | Zero | Bandwidth from your host |
| Durability | 11 nines | 11 nines | Depends on your backups |
| Scaling | Limitless | Limitless | Disk size ceiling |
| Global access | Via CDN or direct | Built on Cloudflare network | Wherever the server is |
| Billing model | Storage + requests + egress | Storage + requests only | Included in server cost |
| Best for | AWS ecosystems | Egress-heavy workloads | Small internal files |
S3 and R2 are functionally similar stores; R2's zero-egress pricing makes it dramatically cheaper for public, traffic-heavy content. Either works behind a CDN — the delivery layer, not the storage, is what users feel.
Implementation
The implementation order: provision the bucket with versioning and lifecycle rules (keep originals, expire old derivatives), put a CDN in front with a custom domain, set cache headers per path pattern — immutable for hashed assets, short for anything that can change — and wire the upload flow to use signed URLs with size and type limits enforced at the bucket policy level, not just in the client.
Then monitor the three numbers that matter: cache hit ratio (should stay above 90% for static assets), origin bandwidth (should fall to near zero after a day of warmup), and storage growth (lifecycle rules keep it honest). If any of the three look wrong, the configuration, not the platform, is usually the problem.
- Client-upload via signed URLs so the origin never handles binaries
- Derivative processing in a trigger function: resize, compress, convert
- Versioned keys: immutable assets get year-long cache headers
- CDN in front with a custom domain and per-path cache policy
- Bucket policies enforce size and type limits — never trust the client
- Lifecycle rules expire old derivatives and keep the bucket honest
Key Decisions
S3 or R2?
Choose by ecosystem and egress. If your stack is already AWS, S3 wins on integration. If your content is public and traffic-heavy — images, videos, files — R2's zero egress makes it the cheaper default, especially at scale. Behind a CDN, the choice barely affects users.
Do I even need a CDN?
If your users are spread across continents, yes — it is the difference between 200ms and 800ms page loads. If all your users are in one region and your origin is well-connected, a CDN is still a cheap insurance layer for spikes. There are few stacks where it is not worth configuring.
Common Mistakes to Avoid
The classic mistakes are billing-shaped: hot-linked images driving surprise egress, cache headers missing so every visit re-downloads from origin, and resizing pipelines that download full images for every thumbnail. Each one is a config fix that saves real money.
The second cluster is security: public buckets with guessable keys (the scanner-honeypot), no signed uploads (the abuse vector), and versioning disabled (the deletion disaster). Object storage is unforgiving of the fundamentals — get the basics right once and never revisit them.
- Public buckets with predictable object keys — a malware scanner's favorite host
- No cache headers: every page load re-fetches every asset from origin
- Resizing full images in the app server instead of in a trigger function
- No lifecycle rules: derivatives accumulate until the bill surprises you
- Egress-unaware designs: hot files served through the origin instead of the CDN
The payoff: infrastructure that runs quietly so the product gets the attention.
Putting It Into Practice
Audit your current asset pipeline this week: where do uploads go, what serves them, what cache headers exist, and what would a hot link cost you? Fix the weakest link — for most sites it is either missing cache headers or missing signed uploads.
For new projects, build the pipeline as bucket + function + CDN from day one. It is an afternoon of configuration and it removes the entire class of storage problems forever.
Key Takeaways
- Object storage is the modern filesystem: durable, limitless, and accessed by URL
- CDNs are the biggest free performance win available to most sites
- Egress is the billing trap — R2's zero egress or a CDN in front neutralizes it
- Signed client uploads keep the origin out of the binary path
- Versioned keys enable year-long cache headers and near-zero origin load
- Bucket policies and lifecycle rules are the security and cost fundamentals
Frequently Asked Questions
Is object storage expensive?
Storage itself is extremely cheap — pennies per GB per month. The costs that surprise people are egress and requests at scale. A CDN in front (or R2's zero egress) keeps the delivery layer off your bill, and lifecycle rules keep storage from drifting.
Can I use object storage for a database?
No — object storage is for files and immutable blobs, not for the small random reads and writes a database needs. Use it for images, videos, documents, and backups; use a real database for data.
Why is my CDN cache hit ratio low?
Almost always cache headers: assets without Cache-Control headers (or with per-user URLs) can't be cached. Set immutable, year-long headers on versioned assets and short headers on dynamic paths, then re-check — ratios above 90% are normal afterwards.
Conclusion
Object storage and CDNs are the quiet backbone of fast delivery: durable storage for everything, edge delivery for everyone, and configuration — not code — doing the heavy lifting.
Build the bucket-plus-function-plus-CDN pattern once, set the cache and lifecycle fundamentals, and the file problems disappear from your life. That is the whole point of infrastructure you can forget.