Data engineering
Part 3 of 6 · Object StorageConsistency — Read-after-write, Listing & Conditional Writes
Modern S3 is strongly consistent for new objects, overwrites, deletes, and listings in-region. Interviews still expect the pre-2020 failure mode, what If-Match buys you, and how replication, CDNs, and client caches put staleness back in front of a strong API.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Shared manifest object
Prefer
Conditional PUT with If-Match
Read the ETag, write only if it still matches, retry on 412. The loser sees the conflict instead of erasing the winner.
- The store rejects a stale ETag.
- You can also publish a new versioned key and flip a pointer.
- The retry must re-read. Replaying the old body is another lost update.
Alternative
Last writer wins
Both clients GET, both PUT, and the second PUT replaces the first with no error. The API can be strongly consistent and still lose an update.
- Strong read-after-write does not mean one writer.
- A later GET shows only the last body. The earlier edit is gone unless versioning kept it.
- Versioning preserves history. It does not stop the race by itself.
Overview
Object stores spent a decade in interview lore as eventually consistent on overwrite and listing. That lore is out of date for Amazon S3. In December 2020, S3 announced strong consistency for new PUTs, overwrites, deletes, and list operations, across storage classes and regions, for the S3 API itself.
The sentence interviewers still want is narrower than "S3 is consistent now." The origin API in one region is strongly consistent after a successful call. Cross-region replication is asynchronous. A CDN serves what it cached. An application cache serves what it stored. Those layers reintroduce staleness on top of a strong API.
Cloud Storage and Azure Blob have their own models. Do not paste the S3 date onto them. Say "check the current origin guarantee, then name the cache and the replica."
Vocabulary
| Term | Meaning | Object-store angle |
|---|---|---|
| Read-after-write | GET sees bytes after a successful PUT of a new key | Strong on modern S3 |
| Read-after-overwrite | GET sees the latest replace | Strong on modern S3; a CDN may not |
| Listing consistency | LIST reflects creates and deletes | Strong on modern S3; you must paginate |
| Conditional write | PUT only if the ETag or version matches | Stops a lost update |
| Client or CDN cache | A copy outside the API | Can serve a deleted object |
The misconception to retire: "S3 is eventually consistent everywhere." The misconception to keep: "strong origin consistency means every cache is fresh."
API versus the layers in front of it
| Layer | Typical guarantee | What the interviewer asks |
|---|---|---|
| Origin API, modern S3 | Strong for PUT, overwrite, delete, and LIST | Two writers and no If-Match |
| Cross-region replication | Eventual across regions | A read of the replica too early |
| CDN or browser cache | TTL or purge | A deleted object still downloads |
| SDK or app cache | Whatever you built | A client that never revalidates |
Flow
- 1
Successful PUT
- nextOrigin API shows it
- 2
Origin API shows it
- nextGET in the same region
- nextLIST in the same bucket
- 3
GET in the same region
- 4
LIST in the same bucket
Lesson map
Consistency — Read-after-write, Listing & Conditional Writes
Modern S3 is strongly consistent for new objects, overwrites, deletes, and listings in-region. Interviews still expect the pre-2020 failure mode, what If-Match buys you, and how replication, CDNs, and client caches put staleness back in front of a strong API.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB put["Successful PUT"] api["Origin API shows it"] get["GET in the same region"] list["LIST in the same bucket"] put -->|Successful PUT to Origin API shows it| api api -->|Origin API shows it| get api -->|Origin API shows it| list
Decisions
- 1
Strong origin API
- nextAnother hop
- ?
Another hop
- async copyReplica region can lag
- cached GETCDN can stay stale
- 3
Replica region can lag
- 4
CDN can stay stale
The dual-writer race
Strong consistency tells you that a successful PUT is visible. It does not merge two writers. Both read ETag e1. A writes with If-Match: e1 and receives e2. B writes with the stale e1 and receives 412 Precondition Failed. Without the condition, B's PUT succeeds and A's body is gone.
S3 conditional writes (If-Match and If-None-Match on PUT) are the current API for this. If-None-Match with * creates the key only when it is absent. If you cannot use conditional writes, publish a new key per attempt and flip a pointer in a store that has a compare-and-swap. Versioning keeps the lost body as a prior version. It does not tell the second writer they lost.
Flow
- 1
1. A and B read ETag e1
- next2. A PUT If-Match e1
- 2
2. A PUT If-Match e1
- next3. Store returns ETag e2
- 3
3. Store returns ETag e2
- next4. B PUT If-Match e1
- 4
4. B PUT If-Match e1
- next5. 412 Precondition Failed
- 5
5. 412 Precondition Failed
- next6. No If-Match means lost update
- 6
6. No If-Match means lost update
Listing, even when the API is strong
A strong LIST can still miss objects because the client stopped early or raced a writer.
- Pagination. Follow
ContinuationTokenor the next page token until it is absent. A single page is not the bucket. - Delimiter.
delimiter=/returns common prefixes plus the objects at that level. That is the folder illusion. It is not a recursive list. - Huge prefixes. Parallelize by sub-prefix (date or hash) when one list cannot finish in time. Prefix shape is the data model.
- Delete markers. In a versioned bucket, the current listing and the all-versions listing disagree on purpose.
- TOCTOU. LIST then GET can still observe a newer writer. Pin the GET with a version id or an ETag if the bytes must match the list you saw.
Incomplete multipart parts are not the object. The key appears when CompleteMultipartUpload succeeds. A LIST between upload-part and complete does not show the final object. That state machine is the next lesson.
What you may assume
Start at the successful API call. Add a caveat for every hop that is not that API.
- 1
Successful PUT or complete
Same-region GET and LIST on modern S3 can see it. A failed call left no new object. - 2
Overwrite without a condition
The latest PUT wins and is immediately visible. The previous body is gone unless versioning kept it. - 3
Conditional PUT
Stale If-Match returns 412. The client re-reads and retries. That is the lost-update control. - 4
Leave the region or the origin
Replication lag and CDN TTL are outside the strong API. Say so before you promise a read.
A bucket that can say 412
No network. Two writers share an in-memory object. The second PUT with the old ETag throws. The body stays with the first writer.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Press Run. Snippets must be self-contained — no network, files, or native modules.
The TypeScript loop is the whole lesson on pagination: stop when the token is missing, not when a page looks small. The Python bucket is the whole lesson on lost updates: a matching ETag is a permit to write, and a stale one is a 412.
Interview Q&A
The client PUTs and immediately LISTs. Can modern S3 miss the object?
Answer
Not for a successful PUT followed by a LIST against the same bucket's S3 API. You can still miss it by stopping at the first page, listing a different prefix, reading a replica region, or reading a CDN. A failed PUT never created it.
How do you prevent a lost update on a shared manifest?
Answer
Conditional PUT with If-Match on the ETag you read, and retry by re-reading after 412. Or write a new key per version and atomically publish a pointer in a database or with a compare-and-swap object. Last-writer-wins PUT is not that control.
Does strong consistency make incomplete multipart parts visible as the object?
Answer
No. Parts are not the object. The key becomes visible when CompleteMultipartUpload succeeds. Abort or a lifecycle abort removes the parts. Until then, GET and LIST do not return the final object.
What did people memorize before December 2020?
Answer
A new object PUT was read-after-write consistent. Overwrites and listings were eventually consistent, so a GET could return the old bytes and a LIST could omit a key you had just written. That is the lore to update, not the current S3 API.
Does strong consistency apply to another region?
Answer
The S3 API in each region is strongly consistent for its own bucket operations. Cross-region replication copies asynchronously. A GET in the destination can run before the copy lands. Do not use the replica as a read-your-writes store.
Why can a deleted object still download?
Answer
The origin DELETE is visible to the next origin GET. A CDN or browser that cached the bytes will keep serving them until TTL, invalidation, or a URL that changed with the version. Origin consistency does not purge edges.
What does If-None-Match star mean?
Answer
Create the object only if the key is absent. A second racer gets a precondition failure instead of an overwrite. It is the create-once form of the same idea as If-Match.
Does versioning stop the race?
Answer
It keeps the overwritten bytes as a prior version. The current version is still whichever PUT finished last. Readers who do not pass a version id see the winner. You still want a condition or a pointer flip if both writers must know they conflicted.
LIST returned a key and GET returned 404. How?
Answer
A concurrent delete, a delete marker that the current list and a later GET disagree on if you listed versions, a wrong key, or a cache. On a versioned bucket, GET the version id you listed. Do not assume the name is stable between the two calls.
Is Cloud Storage the same announcement as S3?
Answer
No. Say the guarantee you actually read for that service. The interview structure is the same: origin API, then replica, then cache, then concurrent writers.
Pitfalls
Timeline one: PUT, then same-region GET and LIST. Mark them visible. Timeline two: the same PUT, then a CDN GET and a replica-region GET. Mark those as maybe stale. Under both, add a second writer without If-Match and cross out the first body.