How Object Storage Actually Works
Try the interactive lab for this articleTake the quiz (6 questions · ~5 min)Object storage looks deceptively simple from the outside. You PUT bytes to a key, you GET them later, you maybe set a lifecycle rule, and that seems to be the whole story. The interface is calmer than a filesystem and much calmer than a database. There is no tree of mutable directories to lock, no in-place overwrite of fixed blocks, no SQL planner, no page cache you need to think about directly. That simplicity is the product surface, not the implementation.
Behind an S3 style API sits a distributed system with at least three hard jobs to solve at once. It needs to accept streams of arbitrary size from clients that can disconnect halfway through. It needs to make a durability promise across disks, servers, racks, and often whole datacentres. It needs to remember where every object lives, which version is current, what checksums apply, what ACL or policy governs access, and how to list millions or billions of keys without turning metadata into a global lock.
The easiest way to see object storage clearly is to drop the metaphor that confuses most people. It is not a remote hard drive. It is not NFS with an HTTP wrapper. It is a durable object namespace where each object is usually treated as an immutable blob plus metadata. Writes create or replace whole object versions. Reads fetch by key. The backend decides how that blob is chunked, placed, replicated, encoded, repaired, and garbage collected.
That design choice is why object storage scaled so well for backups, media libraries, data lakes, package registries, logs, machine learning artefacts, and cloud-native application data. It avoids the hardest parts of POSIX semantics and spends the budget on throughput, durability, and fleet management instead.
This article explains the machinery under that interface. We will look at the request path for a write, why object stores split control plane from data plane, how multipart uploads and manifests work, why erasure coding usually beats raw replication for large objects, how metadata services make list and read-after-write work, what versioning and delete markers really mean, and where the operational pain still lives. By the end, the phrase “stored in S3” should feel less like a black box and more like a specific set of engineering tradeoffs.
The Contract Is Key To Blob, Not Path To Mutable Blocks
A filesystem promises a namespace of directories and files with operations such as open, append, seek, rename, chmod, link, unlink, and often advisory or mandatory locking. Applications can update byte ranges in place. Two clients can open the same file and race with each other. Directory entries are data structures with their own semantics. A storage engine that supports that contract must care about inode lifetimes, per-directory mutation, metadata ordering, and consistency rules that evolved around local Unix machines.
Object storage cuts that contract down aggressively.
The visible model is usually:
- a bucket or container that groups objects under a policy boundary
- a key that names one object inside that bucket
- a blob of bytes, from a few bytes to many terabytes
- a set of metadata fields such as content type, checksum, encryption state, tags, retention policy, or user-defined headers
A key often looks path-like:
logs/2026/06/11/payments/part-00037.parquetThat does not mean the backend has a directory tree called logs, then 2026, then 06, and so on. In many object stores that slash is just another byte in a string. Listing with a delimiter is a presentation feature built on top of flat keys. The store may optimise prefixes because customers use them heavily, but the public contract is usually closer to “lookup by full key” than to “walk a real directory hierarchy”.
The second important contract difference is that object writes are typically whole-object operations. If you upload a new 400 MiB object to the same key, the store does not expose “block 17 changed in place” as a first-class semantic. Internally it may reuse chunks or erasure sets, but the public view is “there is now a new object body for this key” or “there is now a new version for this key if versioning is enabled”.
That restriction buys a lot:
- no shared mutable file offset to coordinate
- no partial overwrite semantics across clients
- no need to expose random-write ordering at the API layer
- much easier cross-node replication and repair
- simpler integrity checking because the object body can be checksummed as a unit or as large parts
It also removes things people sometimes assume exist:
- appending to an object is usually implemented as “create a new object” or multipart composition, not a real append syscall
- renaming a large object is usually copy plus delete, or metadata tricks in a higher layer, not an atomic directory entry move
- updating a ZIP file stored in an object bucket means rewriting the ZIP file, not patching blocks in place
This is why object storage is ideal for blobs that are produced once and consumed many times. Photos, backups, segment files, Parquet partitions, container layers, and log archives fit the model naturally. A virtual machine disk image that needs low-latency 4 KiB overwrites does not. Cloud providers solve that with block storage products, not with object buckets.
A PUT Request Starts In The Metadata Plane Before The Data Plane Finishes
The simplest mental model for a write is “client sends bytes to a server, server writes bytes to disks”. Real object stores separate that into at least two coordinated paths.
The metadata plane answers questions such as:
- does this bucket exist?
- is the caller allowed to write to this key?
- what retention or legal hold rules apply?
- does this key already exist, and if so, is versioning enabled?
- where should the new object or new object parts be placed?
- which checksum, encryption, and storage class metadata will be attached?
- when is the new object considered committed and visible?
The data plane handles the object payload itself:
- accepting the byte stream
- chunking it into parts or extents
- computing checksums
- encrypting if needed
- sending fragments to storage nodes
- waiting for the chosen durability condition
A typical HTTP write request might look like this:
PUT /backups/eu-prod/db-2026-06-11.tar.zst HTTP/1.1
Host: objects.example.net
Content-Length: 734003200
Content-Type: application/zstd
x-amz-checksum-sha256: 4EqeU4sL2...
x-amz-server-side-encryption: AES256
Authorization: AWS4-HMAC-SHA256 Credential=...Before the last byte has arrived, the frontend has already had to do a lot of control work. It authenticates the caller. It authorises the action against bucket policy and possibly per-object ACL rules. It resolves the bucket to the right region or cell. It decides whether this write is a single-part upload, a resumable multipart upload, or a copy operation from another object. It may allocate an internal object identifier that will remain stable even if the visible key is later deleted or overwritten.
In many systems the frontend then asks a placement or metadata service for a storage plan. That plan can be as simple as “replicate this small object to nodes A, D, and G” or as detailed as “for each 128 MiB part, encode 6 data shards plus 2 parity shards and place them across these 8 failure domains”.
The payload stream then flows to storage nodes, often not through one single process all the way. Large systems try to avoid hairpinning terabytes through a central coordinator. The control plane decides; the data plane streams directly to the machines that will persist the bytes.
That is why presigned URLs and upload session tokens are so common. The metadata plane authorises and allocates the upload, then hands the client a time-limited capability that lets it push bytes to the data plane without bouncing every payload byte through the full control path.
This split also explains an operational fact people miss. An object store can have healthy disks and still be effectively down if the metadata plane is unhealthy. If the service cannot create object manifests, enforce versioning rules, or record a committed write, the data plane cannot safely publish new objects even if it can still absorb raw bytes.
The Store Rarely Writes One Giant Blob As One Giant Blob
A 40 KiB avatar image and a 4 TiB backup are both “objects”, but nobody sane stores them with exactly the same internal mechanics.
For small objects, many systems can afford a simple path:
- receive the full body
- compute checksum
- write one or a few internal chunks
- replicate or encode them
- write metadata that points at those chunks
- publish the object as visible
For large objects, the store almost always breaks the upload into independently handled parts. Amazon S3 popularised the multipart upload model, but the idea is universal because it solves several real problems.
First, clients fail. If a 900 GiB upload over a trans-European WAN link drops at 872 GiB, nobody wants to restart from byte zero.
Second, part-level parallelism matters. A client in Madrid with a fast 100 Gbit/s uplink can often fill the pipe only by sending many parts concurrently.
Third, part-level integrity matters. Verifying one checksum over 900 GiB is possible, but verifying checksums for 64 MiB, 128 MiB, or 256 MiB parts gives better failure isolation and easier resume behaviour.
Fourth, part-level placement and repair are easier. A store can rebalance, heal, or garbage collect at part granularity instead of treating the whole object as one indivisible extent.
A multipart upload usually looks like this:
- Initiate upload. The client asks for a new multipart session for
video/2026/keynote.mov. - Receive upload ID. The store returns an internal upload identifier.
- Upload parts. The client uploads numbered parts, often in parallel.
- Validate parts. Each part gets its own checksum or ETag-like identifier.
- Complete upload. The client submits the ordered list of parts that should form the object.
- Commit manifest. The store atomically creates metadata saying “this object version consists of parts 1..N in this order”.
The critical design point is that uploaded parts are usually not yet the visible object. They are staged data. Only the final complete step publishes the manifest that makes the object readable under its key.
That gives the service atomic visibility. A client fetching the key either sees the old object version or the newly committed one. It should not see “the first 57 parts of the new object are already visible while parts 58 to 80 are still uploading”.
An internal manifest row might conceptually look like this:
{
"bucket": "backups",
"key": "eu-prod/db-2026-06-11.tar.zst",
"version_id": "v8f4c2",
"size_bytes": 734003200,
"parts": [
{ "part": 1, "offset": 0, "length": 134217728, "blob_id": "b91" },
{ "part": 2, "offset": 134217728, "length": 134217728, "blob_id": "b92" },
{ "part": 3, "offset": 268435456, "length": 134217728, "blob_id": "b93" },
{ "part": 4, "offset": 402653184, "length": 134217728, "blob_id": "b94" },
{ "part": 5, "offset": 536870912, "length": 134217728, "blob_id": "b95" },
{ "part": 6, "offset": 671088640, "length": 62914560, "blob_id": "b96" }
],
"checksum_sha256": "4EqeU4sL2...",
"storage_class": "standard",
"committed_at": "2026-06-11T01:06:42Z"
}The object itself is now a metadata record that points at durable blobs already written elsewhere.
This is one reason object stores cope well with huge payloads. The visible abstraction is “one object”, but the backend is usually dealing in manifests, chunks, parts, shards, and garbage-collection references.
Durability Usually Means Replicas For Small Objects And Erasure Coding For Large Ones
If you store three copies of every object chunk on three different machines, durability is easy to explain. Lose one disk and you still have two copies. Lose one server and you still have two copies. Lose one rack, if placement was sensible, and you may still have two copies.
The cost is obvious too. Three replicas means roughly 3x raw capacity before metadata, checksums, and background repair traffic. For small objects that overhead can be acceptable because simplicity matters and metadata overhead dominates anyway. For large cold or warm data it gets expensive quickly.
That is why modern object stores lean heavily on erasure coding.
In an erasure-coded layout, the store divides data into k data shards and computes m parity shards. A 6+2 layout means six data fragments plus two parity fragments. Any six of the eight can reconstruct the original data. A 10+4 layout means ten data fragments plus four parity fragments, tolerating four losses.
The storage overhead is then:
overhead = (k + m) / k
6 + 2 => 8 / 6 = 1.33x raw
10 + 4 => 14 / 10 = 1.40x rawCompared with 3x replication, that is a massive capacity win.
The tradeoff is CPU, network, and repair complexity.
A replicated write can often stream the same bytes to three places. An erasure-coded write must segment the input into stripes, compute parity, and place shards across failure domains. A replicated read can fetch any one good copy. An erasure-coded read may need several shards and may need decode work if one shard is missing. Repairing a lost replicated chunk is one source-to-one-target copying. Repairing one lost shard in an erasure set usually means reading enough surviving shards to reconstruct the missing one and then writing it somewhere new.
Object stores often mix strategies by object size or storage class:
- very small objects may be replicated because erasure-coding tiny payloads is inefficient
- hot storage classes may favour lower-latency layouts
- colder classes may use wider erasure sets for better capacity efficiency
- cross-region durability may replicate between regions even if each region uses erasure coding internally
Placement policy matters as much as the coding scheme. “Eight shards” is not safe if five land in the same chassis. Real systems define failure domains such as disk, host, rack, room, availability zone, and region, then place shards so one ordinary failure does not wipe out too much of the set.
A write is only durable when the service has met whatever publish rule it chose. In a replicated store that might mean all three replicas acked, or perhaps two replicas plus a durable write-ahead journal. In an erasure-coded store it might mean all shards for a stripe are persisted, or persisted to enough journaled targets that the remaining work can finish after a crash. The public API rarely exposes the nuance, but the internal contract is the real durability boundary.
This is also where background healing becomes unavoidable. Disks die. Servers disappear for maintenance. Bit rot and latent read errors happen. Object storage durability is not just “we wrote the object once”. It is “we keep checking and restoring redundancy for the lifetime of the object”.
Checksums And Encryption Are Part Of The Storage Protocol, Not Decoration
If object storage is going to hold backups, media masters, package tarballs, or evidence archives for years, it needs stronger integrity machinery than “the disk returned success once”. Good systems verify bytes at several layers.
The first layer is the upload request itself. Clients can send an explicit checksum header, historically Content-MD5 and in newer APIs often SHA-256 or CRC32C variants. The frontend computes the same checksum while streaming the body or each multipart part. If the values differ, the write is rejected before the object becomes visible. That catches corruption between client memory and the service edge.
The second layer is internal fragment integrity. Every stored part or shard usually carries its own checksum in metadata. On read, the service can verify fragments as it fetches them. During background scrubbing it can scan old fragments, recalculate checksums, and compare them with the recorded values without waiting for a user read to discover damage.
That background scrub matters because not every corruption event is a dramatic disk failure. Latent sector errors, controller bugs, firmware regressions, bad RAM in a host without end-to-end protection, or silent bit flips can leave a fragment unreadable months after the original upload. An erasure-coded store can often repair the damaged shard from the surviving ones, but only if it notices the bad shard before too many others fail.
Encryption follows a similarly layered pattern. In server-side encryption the object store usually generates or obtains a per-object data key, encrypts the payload fragments with that symmetric key, then encrypts the data key itself under a higher-level key from a KMS or hardware security module. This is envelope encryption. It means rotating a master key does not require rewriting petabytes of object data immediately. The service can rewrap the small encrypted data keys instead.
Metadata still matters here too. The manifest may need to record:
- which encryption scheme was used
- which KMS key or key version wrapped the data key
- whether checksums are over plaintext, ciphertext, or both
- which caller is allowed to decrypt on read
This is one of the places where object storage stops looking like “just bytes in buckets” and starts looking like a storage operating system. The payload path, metadata path, integrity path, and key-management path all have to agree before a write is truly safe.
Metadata Is The Hard Part Because It Turns Blobs Into A Namespace
If object storage were only “write anonymous blobs to many disks”, the problem would be closer to a backup appliance. What makes an object store useful is the namespace and metadata sitting above those blobs.
The system has to answer efficiently:
- what is the current version of key
invoices/2026/06/apollo.pdf? - which blob parts compose it?
- what are the checksum and content type?
- who may read it?
- when listing
invoices/2026/06/, which keys should appear and in what order? - if versioning is enabled, which version is latest and which older versions still exist?
- if an upload was interrupted, which staged parts are orphaned and may be reclaimed?
That is a database problem, even if the service does not call it that publicly.
Early object stores often had eventual-consistency edges around metadata. You could write an object and then briefly fail to see it in a list or overwrite race. That happened because the system chose high write availability and scale over immediate global agreement on namespace state. Modern flagship systems have pushed much harder toward strong read-after-write consistency because application developers hated having to reason about invisible fresh objects.
To provide stronger semantics, the metadata plane usually needs some combination of:
- partitioned keyspace ownership
- per-partition consensus or primary ownership
- transactional updates for manifest publish
- ordered version numbers or commit timestamps
- durable logs for metadata changes
A common design is to partition buckets or key ranges across many metadata tablets or shards. Each shard owns a slice of the namespace. Within that slice, one leader or primary serialises changes. A new object commit becomes a metadata transaction: create version row, point key to version, update list index, maybe write retention metadata, then acknowledge success.
This looks suspiciously like a database because it is one.
List operations are especially revealing. A flat keyspace plus lexicographic listing sounds easy until one customer writes billions of keys under a hot prefix such as:
telemetry/2026/06/11/14/...If the store naively partitions by leading prefix, all those writes pile onto one shard. Modern stores therefore hash or otherwise spread internal ownership more evenly while still presenting lexicographic list results at the API layer. That can make list more expensive than point lookup because the service may need to merge results from several partitions to present one sorted stream.
This is also why key naming advice exists. Object storage customers learned, sometimes painfully, that “put the timestamp at the start of the key” can concentrate traffic. Providers improved the backend, but application designers still benefit from understanding the load shape their naming scheme creates.
Metadata size itself becomes serious at scale. Ten billion objects with a few hundred bytes of metadata each is already terabytes of namespace data before replication, indexes, or version history. A bucket full of 8 KiB thumbnails can be metadata-heavy in a way a bucket full of 4 GiB database dumps is not.
When engineers say object storage is “simple”, ask whether they mean the blob store or the metadata service. The blob store is where the bytes live. The metadata service is where the system earns its keep.
Reads Are Manifest Lookups Plus Range Fetches, Not File Opens
The read path is the mirror image of the write path, but it has its own optimisations.
A GET starts by resolving the current visible version of the key. If versioning is enabled and the caller requested a specific version ID, the store must resolve that version instead. Then it loads the manifest describing which internal blobs or shards make up the object.
For a small object stored as a replicated blob, the path can be straightforward:
- load metadata
- pick a healthy replica, often the nearest or least loaded
- stream bytes to the client
- include headers such as ETag, checksum, content type, and last-modified
For multipart or erasure-coded objects, the read path becomes more interesting.
A large GET often does range reads, especially for media playback, analytics formats, and resume-capable clients:
GET /lake/events/day=2026-06-11/part-00037.parquet HTTP/1.1
Host: objects.example.net
Range: bytes=134217728-268435455
If-Match: "9f0d8c2a"The object store does not need to pull the full 900 GiB object to satisfy a 16 MiB range. It uses the manifest to identify which parts or stripes intersect that byte range, fetches only those, and returns the selected window.
This is why object storage works well with columnar analytics formats. Engines such as Spark, DuckDB, or Trino can request only the byte ranges they need from Parquet or ORC files instead of downloading the whole object.
Conditional headers matter too:
If-Matchprevents reading stale expectationsIf-None-Matchenables cache validationIf-Modified-Sincereduces unnecessary transfers
The ETag header often confuses people. In some systems and cases it is an MD5 of the object body. In others, especially multipart uploads, it is not a simple content hash at all but an implementation-derived identifier. Treating ETag as a universal checksum is a common bug. Use the explicit checksum features when you actually need checksum semantics.
Read performance depends on where the bottleneck is:
- metadata lookup latency for tiny objects
- aggregate disk and network bandwidth for large sequential reads
- shard fan-out and decode cost for degraded erasure sets
- TLS termination and per-request overhead for many small reads
That last point is why data-lake systems often favour fewer larger objects over trillions of tiny ones. One 512 MiB object is cheap to stream. Sixty-four thousand 8 KiB objects are a tax on metadata, request processing, TLS, TCP, and list overhead.
Caches sit at several layers too. There can be client-side caches, CDN caches for public objects, proxy caches, SSD caches in front of colder media, and per-node read caches for hot fragments. The object interface hides those details, but they dominate real latency.
Delete Usually Means Metadata State Change First And Garbage Collection Later
Deleting a file on a local filesystem often means removing a directory entry and dropping references so blocks can be reclaimed once no process still holds the inode open. Object storage deletion is conceptually similar in one way and very different in another.
The similarity is that delete is usually metadata first. The system records that the key should no longer resolve to the previous current version. If versioning is disabled, the object becomes invisible. If versioning is enabled, many systems create a delete marker that becomes the current visible state while older versions still exist underneath.
The difference is that actual space reclamation can be much more delayed and policy-driven.
Reasons include:
- older object versions may still be retained intentionally
- multipart upload parts may be referenced by an uncompleted session
- replication or erasure repair may still need source fragments
- bucket lifecycle rules may transition objects to colder tiers before deletion
- object lock or legal hold may make physical removal illegal until a timestamp passes
Versioning is the clearest case. Suppose reports/q2.pdf already exists and versioning is on.
- Version
v1is current. - A new upload creates
v2and makes it current. - A delete request creates delete marker
d3. - A plain
GET reports/q2.pdfnow returns not found or delete-marker semantics. - A
GETfor versionv2still works if the caller asks for it and has permission.
That means “delete” and “destroy bytes immediately” are different operations. This distinction matters for ransomware recovery, compliance retention, and accidental deletion recovery.
Lifecycle management builds on the same metadata machinery. A rule might say:
- after 30 days, move logs from standard to infrequent-access
- after 180 days, move them to archive
- after 730 days, delete old versions permanently
The object bytes may move between storage pools, coding schemes, or media types while the visible key remains the same. Again the manifest and metadata plane do the heavy lifting.
Garbage collection then has to find unreferenced blobs safely. If two object versions share underlying chunks, which some systems permit internally, the collector must not reclaim a chunk still referenced by one live manifest. If a multipart upload was abandoned, the collector must know the staging data has no future commit path. If a repair job recreated a missing shard elsewhere, the old partially failed placement may need later cleanup.
This is why object stores accumulate background jobs: lifecycle transitions, version expiration, orphaned multipart cleanup, integrity scrubbing, and placement rebalancing. The API surface looks static. The backend is busy all the time.
Consistency And Concurrency Live In The Namespace, Not In POSIX Locks
Application developers coming from filesystems often ask the wrong concurrency question first. They ask, “what happens if two clients write the same object at once?” as if the answer should resemble two processes writing the same file descriptor.
Object stores usually make that question simpler and harsher.
There is typically no shared append or byte-range mutation model. Concurrent writes resolve at the object-version or key-visibility level. Depending on the system and request conditions, one write may win, the later one may overwrite it, both may become separate versions, or a precondition may reject one of them.
The important tools are generally:
- version IDs if bucket versioning is enabled
- ETag or checksum preconditions such as
If-Match - object generation numbers in some APIs
- lease or manifest indirection built by the application, not by the store
For example, an application that wants safe compare-and-swap semantics might:
- read object metadata and note the current ETag or version ID
- prepare new content elsewhere
- write only if the current version still matches the old condition
If another writer replaced the object in the meantime, the conditional write fails cleanly.
This is enough for many higher-level patterns such as lock files, state documents, package publication manifests, or atomic data-lake pointer swaps. A common data-lake commit pattern is to write a new immutable manifest object, then update one tiny pointer object or catalog entry that declares which snapshot is current. That avoids rewriting large datasets in place.
What object storage does not usually give you is low-latency mutual exclusion for arbitrary concurrent mutation. If your application needs millisecond-scale record updates with transactions across keys, the object store is the wrong primary database. Use a database for coordination and the object store for blobs.
The consistency story also depends on geography. Strong read-after-write consistency inside one region or cell is much easier than synchronous multi-region consistency across Frankfurt, Dublin, and Warsaw. Cross-region replication is often asynchronous by default because synchronous WAN durability adds real latency and availability tradeoffs.
That does not make the service weak. It makes the contract explicit. Engineers get into trouble when they read “strong consistency” and silently expand it to “all regions, all replicas, all caches, instantly”. The actual guarantee is always scoped to a specific namespace and replication topology.
The Failure Modes Are Mostly About Partial State And Background Debt
Object storage looks boring when it works. The interesting engineering is in the places it can fail without losing correctness.
Client disconnects halfway through a large upload
The staged parts exist, but there is no completed manifest. Good design keeps those parts invisible and reclaimable. Bad design leaks partial state forever.
Metadata commit succeeds after some storage nodes timed out
The service must know whether the durability threshold was really met. If publication gets ahead of actual redundancy, a later node loss can turn into silent data loss.
Storage nodes persist shards, but the metadata leader crashes before publishing
On recovery, the system needs idempotent cleanup or completion logic. Those shards may belong to an uncommitted upload, a committed upload whose ack was lost, or a duplicate retry of the same logical operation.
One disk in an erasure set dies months later
The object is still readable, but only if enough shards remain. Now the system owes itself a repair. If repairs lag behind new failures, the probability of unreadable objects rises. Durability is a race between failure rate and healing rate.
List traffic becomes the bottleneck rather than payload traffic
This is common in analytics, machine learning, and backup tooling that enumerates enormous prefixes repeatedly. The bytes are cheap. The namespace walk is expensive.
Tiny-object workloads wreck economics
A bucket holding 50 billion 2 KiB objects can be dominated by metadata IOPS, per-request CPU, and index storage. The raw payload may be the smallest part of the bill.
Customers treat object storage like a filesystem rename target
A data-processing job that “renames” a million output files into place may actually be doing a million copies plus deletes. If the workflow assumes rename atomicity, it can become slow and semantically wrong.
Cross-region replication lags behind compliance expectations
If the business thought “replicated” meant “safely committed in two countries before ack”, but the service was configured for asynchronous replication, the architecture review failed long before the outage.
You can see the common pattern. Most serious failure modes are not “disk broke and everything vanished”. They are mismatches between visible namespace state, hidden storage state, and background work that has not caught up yet.
That is why large object stores obsess over internal idempotency, scrubbers, repair schedulers, checksum verification, manifest journals, and reconciliation tools. The steady state API is simple because the backend is constantly cleaning up after the messy cases.
The Practical Design Rules Are Different From Filesystems And Databases
If you are building on top of object storage, a few design rules go a long way.
Write immutable objects when you can. If you need to update a dataset, publish new objects and switch a small manifest or pointer, rather than rewriting giant blobs in place.
Batch small records into larger objects. Logs, metrics, and events are usually better as compressed segment files than as one object per event.
Use multipart upload for serious payloads. It improves retry behaviour, throughput, and integrity handling.
Treat list as a real operation with a cost. Cache results where sensible. Avoid prefix layouts that force huge repeated scans for routine work.
Do not assume ETag means MD5. Verify the exact service semantics and use explicit checksum APIs when integrity matters.
Understand replication scope. Same-region durability, cross-zone durability, and cross-region disaster tolerance are different promises.
Use conditional writes for coordination. If the correctness of your workflow depends on not clobbering a newer version, use If-Match, version IDs, or an external metadata store.
Be careful with delete in versioned buckets. A delete marker is not the same thing as data destruction.
Monitor background health, not just request success. Repair backlog, scrub failures, replication lag, orphaned multipart count, and metadata-leader stress all matter.
A lot of expensive mistakes happen because teams use object storage successfully for months, then project their local assumptions onto it. They assume path-like keys imply directories. They assume overwrite means block mutation. They assume delete means gone. They assume replication means synchronously safe everywhere. They assume millions of tiny objects are as cheap as a few thousand large ones.
The interface encourages those assumptions because it is intentionally minimal. The backend reality does not.
The Right Mental Model Is A Distributed Manifest Store Backed By Blob Fragments
The cleanest way to think about object storage is this:
- the visible API names immutable or versioned objects by key
- each committed object corresponds to metadata that describes one current logical version
- that metadata points at one or more internal blob fragments or erasure-coded shards
- those fragments are placed across failure domains and continuously checked and repaired
- lifecycle, versioning, delete, replication, and listing are mostly metadata operations first
Once you adopt that model, a lot of behaviour that looked mysterious becomes ordinary.
Multipart uploads are just staged fragments plus a final manifest publish.
Versioning is just namespace indirection that keeps older manifests reachable.
Read-after-write consistency is mostly a metadata-commit property.
Erasure coding is a way to store the payload fragments efficiently while preserving recoverability.
Range requests work because manifests map logical byte offsets to internal parts.
Delete markers are metadata state that hides older data without destroying it immediately.
Object storage won because this architecture matches the dominant cloud blob workload better than POSIX ever did. Most applications do not need a globally shared mutable filesystem for backups, media, model artefacts, package tarballs, or data-lake files. They need durable named blobs, cheap capacity growth, straightforward HTTP access, and background machinery that keeps the bytes alive while the fleet changes underneath.
That is what object storage actually provides. Not a hard drive in the cloud. A distributed namespace of manifests and blobs, engineered so the simple API can survive ugly realities such as node failure, interrupted uploads, huge fan-out, and years of silent hardware decay.
The apparent simplicity is earned. Under the bucket and key lives a storage system doing far more work than the interface admits.