Under the Hood
Building blocks

Object storage & the blob problem

Where do the files go — the images, videos, PDFs, receipts, backups? Not in the database, and not on a server's local disk; both answers break in ways this lesson makes concrete. Object storage (S3, R2, GCS) is the built-for-it answer: a flat, HTTP-addressable, effectively infinite, absurdly durable store of blobs. This lesson explains how it differs from file and block storage, the properties that define it, and the one pattern you must know to use it well — presigned URLs, which let a client upload and download bytes directly without them ever touching your server. It ends on the two classic ways object storage leaks private data or melts your API.

Object storage & the blob problem

Every app eventually has to store a blob — a binary lump that isn't structured rows: a profile photo, a receipt image, an uploaded PDF, a video, a database backup. And the first two places a developer reaches for are both wrong.

Not the database. You can store a 5MB image as a bytea column, and you'll regret it. Large binaries bloat the table, evict useful rows from the buffer pool (every page of image data is a page of index or row data you're not caching), make every backup enormous and slow, and relational databases are simply not built to stream large binaries to clients efficiently. Blobs in the database poison the thing the database is good at.

Not the server's local disk. Writing uploads to the API server's filesystem seems simple until: the file only exists on that one server (so a load-balanced fleet can't find each other's uploads), it vanishes if the server is replaced (and cloud servers are cattle, not pets — they get replaced), and one machine's disk is a hard capacity ceiling. Local disk breaks horizontal scaling and durability at the same time.

The purpose-built answer is object storage: Amazon S3, Cloudflare R2, Google Cloud Storage. It solves exactly the blob problem, and understanding its shape tells you why it's a different tool from a database or a disk.

Three kinds of storage, and why object storage scales

It helps to place object storage against its cousins:

  • Block storage (a raw disk, an EBS volume) — exposes raw fixed-size blocks; one machine mounts it and puts a filesystem on top. Low-level, fast, single-attach. This is what your database's disk is.
  • File storage (NFS, a network share) — a hierarchical tree of directories and files, mountable by several machines. Familiar, but the hierarchy and POSIX semantics (locking, in-place edits) are exactly what make it hard to scale to billions of files.
  • Object storage — a flat namespace of objects, each an opaque blob plus metadata, addressed by a key, accessed over an HTTP API (PUT, GET, DELETE). No mount, no directory tree, no in-place editing.

That last one's constraints are precisely what let it scale to effectively infinite size. Because there's no hierarchy to traverse, no filesystem to mount, and objects are treated as immutable wholes, the storage system is free to spread objects across an enormous fleet of machines and replicate each one heavily. You give up in-place edits (to "change" an object you replace the whole thing) and you accept higher per-operation latency than a local disk — and in return you get a store that never fills up and effectively never loses data.

The properties that define it

  • Durability that rounds to "never lose it." Providers advertise eleven nines — 99.999999999% annual durability — achieved by transparently replicating each object across many devices and availability zones. You do not run backups of S3; the durability is the point.
  • Flat namespace, fake folders. There's a bucket and a key. photos/2026/07/receipt.jpg looks like folders but is one flat key string; the slashes are convention, and listing "a folder" is really "list keys with this prefix." There is no real directory.
  • Immutable objects. You replace an object, you don't edit it in place. Optional versioning keeps old versions instead of overwriting.
  • Metadata and content-type. Each object carries a content-type and custom metadata, so a GET can be served correctly by a browser or CDN.
  • Read-after-write consistency. Historically S3 was eventually consistent (write an object, a read a moment later might 404) — a famous footgun. Modern S3 offers strong read-after-write consistency, but the history is worth knowing because older systems and some providers still surprise you.

The pattern you must know: presigned URLs

Here's the question that decides whether you use object storage well or badly: a mobile client needs to upload a 5MB receipt and later download it — how do the bytes flow?

The naive answer routes them through your API: client uploads to your server, your server writes to S3; to download, your server reads from S3 and streams to the client. This is a disaster at any scale. Your API is now proxying every byte of every file — it's on the critical path for all media bandwidth, it buffers large files in memory (hello, out-of-memory on a big upload), and you're paying to move bytes through a server that adds nothing but a bottleneck. Media traffic dwarfs API traffic, and you've funneled it through the one component you most need to stay lean.

The right answer is the presigned URL. Your server doesn't move the bytes — it grants permission to move them:

  1. The client asks your API "I want to upload a receipt."
  2. Your API generates a presigned URL — a URL to the object-storage bucket with a cryptographic signature baked into the query string that encodes exactly one permitted operation (PUT this specific key), a short expiry, and nothing more. Generating it is pure computation — no bytes touch your server.
  3. The client PUTs the file directly to object storage using that URL. S3/R2 verifies the signature and accepts the upload.
  4. Downloads work the same way in reverse: your API hands out a presigned GET URL (or a public URL) for one object, and the client fetches it straight from storage — ideally through a CDN that caches it at the edge.

Upload a few receipts each way and watch the meters. Through the API, every megabyte crosses your server (twice); with a presigned URL, your API only signs a token and the bytes go client → storage directly, so "bytes through your API" stays flat no matter how many files upload.

Upload path
Clientthe phone
Your APIbuffers 5 MB
S3 / R2object store
0files uploaded
0 MBbytes through your API
0 MBdirect client → storage

Your server's job is authorization, not transport. Route bytes through the API and it's on the critical path for all media bandwidth — buffering big files in memory (hello OOM) and paying to move gigabytes through the one component you most need lean. A presigned URL is pure computation: the API signs a token granting one operation (PUT this key, short expiry) and the client uploads directly to storage. Watch “bytes through your API” stay flat no matter how many files upload — that's how every serious app does uploads.

The insight: your server's job is authorization, not transport. It decides who may do what to which object and signs a token saying so; the actual gigabytes flow directly between client and storage, on a path your API never sits on. This is how every serious app does uploads and private downloads, and it's the single most important thing to take from this lesson.

Cost, and why the provider choice matters

Object storage pricing has three parts: storage (cheap per GB-month), requests (tiny per-operation charges that add up at high volume), and egress — the charge to move data out to the internet, which is historically where the real money is. Egress is exactly why provider choice matters for a media-heavy app: some providers charge steeply for it, while Cloudflare R2 charges zero egress, which can be the difference between affordable and ruinous when users are downloading images all day. Storage classes (hot vs. infrequent-access vs. cold archive like Glacier) trade retrieval latency and cost for rarely-touched data, but for user-facing media you stay in the hot tier and let a CDN absorb the repeat reads.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Give the concrete reason storing a 5MB image is wrong in (a) a Postgres bytea column and (b) the API server's local disk. Tie the first back to the buffer pool and the second to horizontal scaling.
  2. Object storage is a flat, HTTP-addressed, immutable-object store. Explain how each of those three constraints is what lets it scale to effectively infinite size, and what you give up in exchange.
  3. A mobile client must upload a 5MB receipt. Contrast routing the bytes through your API with the presigned-URL flow, and state the one-sentence principle about what your server's actual job is.
  4. What exactly does a presigned URL encode, and why do "one operation, one key, short expiry" each matter for security? What goes wrong with a broad or long-lived presign?
  5. Public buckets are behind many major breaches. Explain the failure and the private-by-default + presigned-URL discipline that prevents it.
  6. Object-storage cost has three parts. Name them, explain why egress often dominates for a media app, and why that made R2 the right choice for Fable over a steep-egress provider.