The question this answers
How does a key turn into bytes on a specific set of disks, and what does the flat namespace cost you?
A durable, immutable value bound to a key: once a PUT is acknowledged, that exact byte sequence is retrievable and is not modified in place by anything. Modern major stores give read-after-write for a new key and for overwrites of an existing key; listings are a separate index and have historically been weaker than object reads, so a key can be readable before it appears in a list.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A storage node knows which object fragments are on its disks. The routing layer knows a placement function and a cluster map version — and a node acting on a stale cluster map will compute a *different* placement for the same key than its peers, which is the origin of most placement bugs. No node knows whether a listing index has caught up with the writes that have already been acknowledged; that is a second system with its own lag.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
Key → placement → nodes
An object store’s core is smaller than it looks. A key such as photos/2026/cat.jpg is not a path; it is an opaque string containing slashes. The store applies a placement function to it — in practice a hash, so Hash Partitioning and the Modulo Trap and The Ring: Keeping the Mapping Stable When Membership Changes are exactly the mechanism — and gets a placement group, which the cluster map turns into an ordered list of storage nodes. The bytes are then written to those nodes as whole replicas or as erasure-coded fragments.
The consequence of hashing is that there is no directory tree anywhere. photos/2026/ is not a thing that exists; it is a *prefix*, and listing by prefix is served by a separate index over keys, not by walking a structure. That is why an object store has no atomic rename: renaming would mean copying every object to a new key, because the key determines the placement. A "rename a folder" operation on a million objects is a million copies and a million deletes, and every tool that offers it is hiding that fact.
Two properties fall out of this and explain most of the ergonomics. First, objects are immutable: you replace an object wholly, you do not edit its middle. This removes the entire class of concurrent-write problems that Distributed File Systems: Chunks, a Metadata Service, and Where the Copies Go had to define semantics for — there is nothing to interleave. Second, there is no coordinator on the data path at all; the client hashes, or a stateless front end hashes, and the request goes straight to the nodes holding the data. The metadata master that bounded a chunked file system to one machine’s memory simply does not exist, which is why an object store holds trillions of objects without noticing.
Two consistency stories, not one
The most common object-storage bug is treating the store as one system when it is two: an object store keyed by exact key, and a listing index keyed by prefix. They have different consistency properties, and code that writes an object then lists the prefix to find it is depending on the weaker of the two.
For the object itself, the major providers now offer read-after-write consistency: a successful PUT is immediately visible to a subsequent GET of the same key, including overwrites and deletes. This was not always true — for years, overwrite and delete were eventually consistent, and an enormous amount of production code was written against the weaker model. If you are reading an old design document, or running a self-hosted store, check rather than assume.
For listings, be much more careful. Even where a provider documents strong consistency for listings, a great deal of tooling sits on top of a *cached* or *derived* index — an inventory report, a manifest, a metadata database updated by an event notification. Each of those hops is Eventual Consistency: If Updates Stop, Replicas Converge with its own lag, and the notification delivery is Where You Put the Acknowledgement Decides Everything with its own duplicates. The pattern that survives is: do not discover your own writes by listing. Write the key you are going to read, and read that key.
| Operation | Typical guarantee today | What breaks if you assume more |
|---|---|---|
| PUT new key, then GET that keytypical | Read-after-write | Nothing — this is the path to rely on |
| PUT over existing key, then GETtypical | Read-after-write on major providers; historically eventual | A reader gets the previous version and processes stale input with no error |
| DELETE, then GETtypical | Read-after-delete on major providers; historically eventual | A deleted object is served for some seconds after the delete returned 204 |
| PUT, then LIST the prefixassumption | Strong on major providers; weaker on derived indexes and self-hosted stores | A job lists its own inputs, misses the newest file, and silently produces a partial result |
| PUT, then read a metadata DB updated by an eventprotocol | Eventual, at-least-once, out of order | Duplicate processing and a window where the row does not exist yet |
| Concurrent PUT of the same key from two writersprotocol | Last writer wins; no ordering guarantee between them | One writer’s data vanishes with both PUTs returning 200 |
Where the durability number comes from
The eleven-nines durability figure attached to these services is not marketing about disks being good. It is arithmetic over erasure coding across failure domains. An object is split into k data fragments plus m parity fragments and any k of the k+m suffice to reconstruct it, with the fragments placed in separate racks or zones. You then continuously scrub and repair, so the system spends almost all of its time at full redundancy and only briefly at reduced redundancy after a failure. The number falls out of how quickly repair restores the margin, not out of how rarely disks die.
Two honest observations follow. First, the number describes the store losing your bytes, and that is essentially never your incident. Your incident is a lifecycle rule that expired objects you still needed, a bucket policy that let something delete them, or a job that overwrote a good object with a bad one — none of which durability arithmetic covers. Versioning and a separate retention boundary do; see the Cloud domain for how those are configured.
Second, erasure coding trades a small-read penalty for a large space saving: reconstructing an object touches k nodes rather than one, so a small object read has a wider fan-out and inherits Fan Out to 100 and the Component’s Tail Becomes the System’s Median. This is why stores commonly replicate small objects and erasure-code large ones — the same trade that appears every time this domain meets a fan-out.
- Durability comes from erasure coding across failure domains plus fast repair, not from reliable disks.
- The published durability figure says nothing about deletion by policy, permission, or your own bug.
- Erasure coding saves space and widens the read fan-out; replication is cheaper to read and more expensive to store.
- Reduced-redundancy windows after a failure are where the risk actually lives, so repair speed is the durability lever.
What you give up, stated plainly
A flat, immutable, hash-placed namespace removes a lot of problems, and it removes some capabilities you will miss on a specific Tuesday. There is no atomic rename and therefore no atomic "publish this directory" — the standard workaround is to write objects under a temporary prefix and then write one small manifest object whose PUT is the atomic publish point. There is no append and no partial update; changing one byte means rewriting the object, which is why log-structured formats and multi-part uploads exist. There is no cross-object atomicity, which is the same Cross-Partition Operations: Paying for What the Split Took Away problem in different clothing: two objects that must change together cannot.
And there is a per-request cost profile that surprises people who think of it as a disk. Every GET is a network request with a request charge, a first-byte latency of tens of milliseconds, and — if the caller is in another region or leaving the provider — an egress bill. A workload that would be one sequential file read on a local disk becomes ten thousand billed requests. That is not a performance footnote; it changes which algorithms are affordable, which is a large part of why Move the Computation to the Data behaves differently once storage is behind an object API.
Key points
- The key is hashed to a placement, so there is no directory tree and no atomic rename — a "folder rename" is a copy of every object.
- Objects are immutable: you replace them whole, which deletes the entire concurrent-write problem.
- There is no metadata master on the data path, which is why the namespace scales past anything a chunked file system can hold.
- Object reads and prefix listings are two systems with two consistency stories; never discover your own writes by listing.
- Durability comes from erasure coding across failure domains plus fast repair — and covers none of the ways you will actually lose data.
- Per-request cost and first-byte latency make an object store a network service, not a disk.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • A client PUTs a key and a byte stream to a stateless front end, which authenticates and authorises the request.
- • The front end applies the placement function to the key and consults the current cluster map to get an ordered list of nodes.
- • The bytes are written as whole replicas or as k+m erasure-coded fragments spread across failure domains.
- • Once enough fragments are durable, the PUT is acknowledged; the object is now immutable under that key and version.
- • A listing index is updated so the key appears under its prefix — a separate system on its own path.
- • Background scrubbers verify fragments and rebuild any that are missing or corrupt, restoring full redundancy.
- • A GET reverses the process: hash, locate, fetch enough fragments, reconstruct, stream.
- • A front end holds a stale cluster map and writes fragments to the placement the map used to specify.
- • A node fails during a write and the acknowledgement rule was satisfied by fewer durable fragments than intended.
- • The listing index lags behind acknowledged object writes.
- • Two clients PUT the same key concurrently and one version disappears with no error.
- • A lifecycle or retention rule deletes objects that are still referenced by something the rule’s author did not know about.
- • The job that missed a file: a batch job lists a prefix, processes what it finds, and reports success. Output row counts are quietly low, no error anywhere — the newest objects were not in the listing it read.
- • Silent overwrite: two writers PUT the same key; both get 200, one version is gone forever. The only evidence is a version history, if versioning was enabled before the fact.
- • The listing bill: an operator sees cost climb with no traffic change, and finds a job doing a full prefix listing of millions of keys on every run. Request charges, not storage, are the line item.
- • Deleted by policy: a lifecycle rule transitions objects to archive or expires them, and a reader that worked for a year starts failing with 404 or with restore-required errors on old keys.
- • Small-object tail: p99 GET latency is many times p50 for small objects, because each read fans out to k fragment nodes and inherits the slowest one.
- • None on the data path for a single object: hash, write, done. This absence is the entire scalability argument.
- • The cluster map is agreed state and must be versioned; a front end acting on an old map computes a placement its peers disagree with.
- • Cross-object atomicity requires coordination the store does not offer — a manifest object written last is the usual substitute, and it is a convention, not a guarantee.
- • Conditional writes (write-if-absent, write-if-matches-etag) are the one coordination primitive commonly available, and they are enough to build a lock or a single-writer protocol where the provider supports them.
- • Acknowledged objects survive the loss of up to m fragments — that is, the loss of m failure domains — for as long as repair keeps up.
- • During a partial outage, reads of unaffected keys are entirely unaffected: there is no shared coordinator to take the whole namespace down.
- • Listings may fall further behind while object reads and writes continue to work normally.
- • A regional outage takes the bucket with it unless cross-region replication was configured beforehand — replication is not retroactive.
- • Detect: scrubbing finds missing or corrupt fragments before a read does; alert on reduced-redundancy object count rather than on read errors.
- • Contain: enable versioning *before* you need it, so a bad overwrite is a recoverable event rather than a permanent one.
- • Recover: for your own bad writes, restore the previous version. For provider-side fragment loss, repair is automatic and not yours to run.
- • Reconcile: compare an authoritative manifest against a listing to find objects you believe exist and cannot see, which distinguishes "never written" from "not indexed yet".
- • Verify: read back by exact key, never by listing — the listing is the thing you are trying to check.
- • Request counts by operation type, separated: LIST is a different cost and a different failure mode from GET.
- • p50 and p99 first-byte latency by object size class; small objects and large objects fail differently.
- • 404 rate on keys your own system wrote, which is the signature of a listing-lag or lifecycle problem rather than a client bug.
- • Object count per prefix, trended — an unbounded prefix makes listings slower every day until they time out.
- • Egress bytes by destination, which is where the surprise on the invoice lives.
- • Large immutable artefacts: media, backups, build outputs, data-lake files, model weights. Write once, read many, never edit.
- • Any workload where you want durability you did not have to engineer and a namespace with no size limit.
- • Decoupling producers and consumers: the object is the interface, and the two sides never have to be up at the same time.
- • Many small reads and writes: per-request latency and per-request charges dominate, and the workload wants a database.
- • Anything needing in-place update, append, or atomic rename — you will build an increasingly elaborate manifest scheme to fake them.
- • Low-latency serving without a cache in front: tens of milliseconds to first byte is fine for a batch job and not fine for a page render.
- • Using a prefix listing as a work queue, which is the single most common object-storage anti-pattern and fails exactly when the prefix gets large.
- • A database, when you want to query by anything other than the exact key or a prefix of it.
- • A block volume, when you need in-place update or a real file system with POSIX semantics.
- • A message queue or log for work hand-off, instead of listing a prefix to discover new work — that is what Work Queues: One Task, One Worker, Competing Consumers and The Log Is Not a Queue are for.
- • A CDN or cache in front, when the same objects are read repeatedly and the request cost or first-byte latency is the problem.
Key → hash → placement group → disks, and what k+m buys
hash("photos/2026/cat.jpg") → PG 100
PG 100 → zone-B / node-2
zone-B / node-1
zone-C / node-4
zone-A / node-2
zone-B / node-3
zone-A / node-3
zone-A / node-4
zone-B / node-4
zone-C / node-2| Operation | Typical guarantee today | What breaks if you assume more |
|---|---|---|
| PUT new key, then GET that keytypical | Read-after-write | Nothing — this is the path to rely on |
| PUT over existing key, then GETtypical | Read-after-write on major providers; historically eventual | A reader gets the previous version and processes stale input with no error |
| DELETE, then GETtypical | Read-after-delete on major providers; historically eventual | A deleted object is served for some seconds after the delete returned 204 |
| PUT, then LIST the prefixassumption | Strong on major providers; weaker on derived indexes and self-hosted stores | A job lists its own inputs, misses the newest file, and silently produces a partial result |
| PUT, then read a metadata DB updated by an eventprotocol | Eventual, at-least-once, out of order | Duplicate processing and a window where the row does not exist yet |
| Concurrent PUT of the same key from two writersprotocol | Last writer wins; no ordering guarantee between them | One writer's data vanishes with both PUTs returning 200 |
What people believe, and what is true
Folders exist in object storage.
The slash is a character in the key. A "folder" is a prefix, and listing by prefix is a query against a separate index.
Eleven nines of durability means my data is safe.
It means the store is unlikely to lose the bytes. Lifecycle rules, permissions and your own overwrite are far more likely, and none of them are covered by that figure.
I can use a prefix listing to find new work.
Listings are an index with lag, and listing cost grows with the number of objects under the prefix. Use an event or a queue.
Object storage is just a very large disk.
It is a network service with per-request charges, tens of milliseconds of first-byte latency, and no in-place update. Algorithms that were free on a disk are billed here.
Renaming a prefix is cheap because it only touches metadata.
The key determines the placement, so a rename is a full copy plus a delete for every object.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Hash the key to decide which machines hold the bytes. Objects are immutable and the namespace is flat, so there is no coordinator, no directory tree and no atomic rename. Read by exact key, not by listing.
Practical
Enable versioning before you need it. Never discover your own writes with LIST — write a manifest and read that. Watch request counts by operation and object count per prefix; both grow into failures that look like nothing until they are sudden. Assume anything downstream of an event notification is eventually consistent and may deliver twice.
Advanced
The design is the mirror image of Distributed File Systems: Chunks, a Metadata Service, and Where the Copies Go: that architecture concentrates all coordination in one metadata service so the data plane needs none, and pays with a namespace bounded by one machine’s memory. This one removes the metadata service entirely by making the key its own routing decision, and pays by losing every operation a namespace structure would have given you — rename, append, listing that is free. Both are the same trade, made in opposite directions, and knowing which one you are holding tells you which workarounds you are going to need.
Apply it
- 💬 A job writes 10,000 objects then lists the prefix to process them and finds 9,987. What happened, and what is the fix?
- 💬 Why is there no atomic rename, and what do teams build instead?
- 💬 Where does the eleven-nines durability number come from, and what does it not cover?
- 💬 When would you replicate an object rather than erasure-code it?