The question this answers
Where do large, immutable files live when they must be durable, cheap and reachable from every part of the system?
Users upload profile photos and PDF invoices. The files are large, they are never edited in place, several services need to read them, and nobody wants a 4 MB image sitting in a database row that the query planner has to move around.
A flat, effectively unbounded key → bytes namespace with provider-managed durability, reachable over HTTP by any identity you authorize, with per-object metadata and versioning.
Bucket, key, object — and the flat namespace nobody believes at first
The whole model is three nouns. A bucket is a named container and the unit of most policy: access, encryption default, versioning, lifecycle and often region. A key is the full string that identifies one object inside it — users/8123/avatar/2024-06-01.jpg is a key, not a path. The object is the bytes plus metadata: content type, size, an entity tag, a version identifier, and whatever custom headers you attach.
The namespace is flat. There are no directories; the console draws them by splitting on / for your comfort. That has a real consequence: "list this folder" is a prefix scan over a sorted key space, its cost grows with the number of matching keys, and it is paginated. Code that lists a prefix to find out what exists is doing a scan, and it will get slower every month.
Objects are immutable in practice. You replace an object by writing the whole thing again; with versioning on, the old bytes stay and you get a new version identifier. This is why object storage is superb for uploads, backups, build artifacts, logs and media, and useless for a counter you increment.
It is not a database, and the ways that bite are specific
Teams reach for object storage as a general data store because it is cheap and never fills up. Then they discover the four things it does not do, usually one incident at a time.
There are no transactions. Two writes to two keys are two independent operations, and there is no way to make them atomic. The classic version of this bug is an upload flow that writes the object first and the database row second: the process dies in between, and now you have an orphan blob nobody can find and no way to detect it except a reconciliation job somebody has to write.
There are no joins and no query language. Finding "every invoice for customer 8123 issued in March" is a prefix scan if you were disciplined about key naming, and a full-bucket crawl if you were not. The index has to live somewhere else — which is exactly why the standard pattern is *metadata in the database, bytes in the bucket*.
Listing is weakly ordered and eventually complete in enough situations to matter. Many providers now offer strong read-after-write for a single object, and listing semantics are still the part that differs and the part people design around by accident. Do not build correctness on "I listed the prefix and it was all there."
And every operation costs a request. A million tiny objects is a fine capacity story and a terrible bill, because the per-request meter dwarfs the per-gigabyte one at that shape.
| Need | Relational database | Object storage |
|---|---|---|
| Atomic multi-write | A transaction | Nothing. Write your own reconciliation. |
| Query by attribute | WHERE customer_id = 8123 | Prefix scan, if the key encodes it |
| Join two entities | A join | Two round trips and application code |
| Increment a value | UPDATE … SET n = n + 1 | Read, modify, write whole object, lose a concurrent update |
| Store a 40 MB video | Possible and painful — it bloats every backup and buffer | The native case |
| Serve that video to 200k users | A very bad day | Bucket behind a CDN — see CDN as Infrastructure |
The bill has three meters, and only one of them is storage
Storage is the meter everyone budgets for and rarely the one that surprises. Requests surprise workloads with many small objects — a log shipper writing one object per second per instance generates millions of PUT operations a month, and the storage footprint is trivial. Egress surprises anyone serving objects directly to the public internet instead of through a cache; see Egress: Moving Data Costs Money, Not Just Storing It.
The fourth line item is the one that is invisible until an audit: objects nobody deletes. Versioning keeps every overwrite, incomplete multipart uploads keep their parts, and a bucket with no lifecycle rule grows monotonically for the life of the company. A lifecycle policy that expires old versions and aborts stale multipart uploads is the highest-value ten lines of configuration in this module — see Storage Lifecycle: Hot, Warm, Archive, Delete.
Bars are relative weights, not currency. Real rates depend on provider, region, commitment and volume.
Key points
- Bucket → key → object bytes + metadata. The namespace is flat; directories are a display convention over a prefix scan.
- Objects are replaced whole, not edited, which makes the model excellent for uploads, backups, artifacts and media.
- It is not a relational database: no transactions, no joins, no query language, and listing semantics you should not build correctness on.
- The standard pattern is metadata in the database and bytes in the bucket, joined by the key.
- Three meters bill you — storage, requests and egress — and a fourth grows quietly: versions and orphaned multipart parts nobody expires.
The loop, answered
Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.
- • The client signs an HTTP request with an identity the bucket policy recognizes and addresses
bucket + key. - • The service writes the bytes to multiple devices across failure domains before acknowledging, which is where the durability guarantee comes from.
- • Metadata — content type, entity tag, version identifier, custom headers — is stored alongside and returned on read.
- • A read is one HTTP
GETagainst the key; with a CDN in front, most reads never reach the bucket at all. - • Lifecycle rules run asynchronously against the bucket, transitioning or expiring objects by age, prefix or version.
- • Own the key schema. It is the only index you have, and changing it later means rewriting every object.
- • Own lifecycle rules, including expiring non-current versions and aborting incomplete multipart uploads.
- • Own reconciliation between bucket contents and database rows, because nothing makes the two writes atomic.
- • Own access policy: public-by-default is the single most reported cloud misconfiguration, and it is a one-line mistake.
- • Orphaned objects after a failed request — bytes with no database row, invisible and permanently billed.
- • Throttling on a hot key prefix during a traffic spike; the application sees 503s from storage and must retry with backoff.
- • A listing-based worker that processes the same key twice, or misses one, because listing was assumed to be a queue.
- • An overwrite that destroys the previous object because versioning was never enabled — no undo, no restore point.
- • A public bucket policy applied to fix a broken image, which also exposes every other key in the bucket.
- • Capacity effectively never runs out; you will not be the first to fill it.
- • Request rate per key prefix is the real ceiling. Providers partition by key range, so keys that share a long common prefix concentrate on one partition.
- • High-entropy leading characters in a key spread load; a timestamp prefix concentrates every write of the current minute onto the same partition.
- • Read scaling is a CDN problem, not a storage problem, once the same objects are being fetched repeatedly.
- • The trust boundary is identity, not network position: the bucket is reachable from the internet by design, and only policy stops a request.
- • Public read on a bucket is a legitimate design for a static website and a serious finding for user uploads. Context decides — see Public Exposure, Read With Context.
- • Grant per-prefix, not per-bucket. A service that writes
uploads/has no reason to readinvoices/. - • Encryption at rest is on by default at most providers; the interesting question is which key and who may decrypt it. See Key Management and Encryption at Rest.
- • Turn on access logging before you need it. It is the only record of who read an object during an investigation.
- • Per gigabyte stored, per storage class — cold classes trade retrieval latency and a retrieval fee for a lower resting price.
- • Per request, which becomes the dominant meter for many small objects.
- • Per gigabyte egressed to the internet; traffic to a CDN or to a private endpoint inside the provider network is usually cheaper or free.
- • Retained versions and abandoned multipart parts, which accumulate silently and are billed as ordinary storage.
- • Request counts and error rates split by operation — a rising 503 rate on
PUTis prefix throttling, not an outage. - • Object count and total size trended over months, which is how you notice a lifecycle rule that was never written.
- • Access-log reads on sensitive prefixes, which is both a security and a "who still depends on this" signal.
- • The signal that lies: bucket availability. It is essentially always green, including while your application is failing every upload because its credentials expired.
- • A column in the database. A 20 KB avatar for 5,000 users is fine as a
byteacolumn, and it comes with transactions and a single backup — see Managed Databases. - • The local disk on the instance, for anything genuinely temporary. A scratch file for a 200 ms transformation does not need a durable global namespace.
- • A CDN with an origin you already have, when the goal is only serving static assets fast and the assets are built artifacts rather than user data.
- • A managed database's large-object support or a document store, when the blobs are small, transactional and always read together with their metadata.
- • Buys effectively infinite, cheap, durable capacity; costs you transactions, queries, and in-place modification.
- • Buys reach from anywhere with credentials; costs you a surface that is exposed by policy alone, with no network position to hide behind.
- • Buys a very low price per gigabyte; costs you a per-request meter that can dominate the bill at a small-object shape.
- • Buys provider-managed durability; costs you the discipline of lifecycle, versioning and reconciliation that nobody assigns an owner to.
What people believe, and what is true
Buckets have folders.
They have keys. The console splits on / for display. "List a folder" is a paginated prefix scan whose cost grows with the number of keys.
Object storage can replace a database for simple data.
Only if you need no transactions, no queries and no concurrent updates. The moment two writes must both succeed, you are writing your own reconciliation job.
Storage is the object-storage bill.
Requests and egress routinely exceed it. A log shipper writing one small object per second bills almost entirely on requests.