The question this answers
Which storage contract does this data need — a whole-blob namespace, a raw device, or a filesystem several machines share?
One ordinary product writes three unrelated kinds of data: user-uploaded profile photos, a PostgreSQL data directory, and a folder of render assets that four workers read concurrently. No single storage service serves all three well, and the difference is not price.
A vocabulary that survives providers: the unit you address, the operations you are given, how many machines may attach at once, and what one operation costs in latency.
The contract is the unit you address and the operations you get
Every storage decision reduces to four questions, and none of them is "which service is cheapest". *What is the smallest thing I can address?* *Can I modify part of it, or must I replace the whole thing?* *How many machines can attach at once?* *What does one operation cost me in latency?* Answer those and the storage type falls out; skip them and you end up running a database on a network filesystem because it was the box already on the diagram.
Object storage addresses a whole object by a key over HTTP. There is no seek, no partial write in the general case, no directory that means anything to the system — the slashes in a key are characters. You PUT an object and you GET an object, and both are one network round trip with the tail latency of a network round trip.
Block storage addresses fixed-size blocks on a device. It is deliberately dumb: it knows nothing about files, and it is your operating system's filesystem driver that turns blocks into inodes, directories and byte ranges — this is exactly the layering the Operating Systems domain teaches under File Systems: From Path to Blocks. Because the device is dumb, it is fast and it supports random writes, which is why databases live on it.
File storage sits between them: a real filesystem, with POSIX semantics — locks, permissions, rename, partial writes — exported over a network protocol so more than one machine can mount it at the same time. You buy sharing and you pay for it in per-operation latency, because every stat is now a network call.
| Object | Block | File | |
|---|---|---|---|
| Addressed unit | Whole object, by key | Fixed-size block on a device | File and byte range, by path |
| Partial modification | Replace the object (some providers offer ranged writes; do not design around it) | Yes — random writes are the point | Yes, with POSIX semantics |
| Attached by | Anyone with credentials, over HTTP, from anywhere | Usually exactly one instance at a time | Many machines, concurrently |
| Latency shape | Network round trip per operation, high tail | Sub-millisecond to low-millisecond, tight tail | Network round trip per *filesystem operation* |
| Capacity model | Effectively unbounded; you never provision size | Provisioned; you resize it and you can fill it | Usually elastic, but throughput is provisioned |
| Native fit | Uploads, backups, logs, static assets, data lakes | Database files, VM root disks, write-ahead logs | Shared assets, legacy apps that need a mount |
Where each contract attaches in the stack
Drawing the layers makes the trade-offs stop being arbitrary. Block storage attaches *below* the filesystem, which is why the guest OS sees a disk and why a database can control its own page layout and fsync behaviour. File storage attaches *at* the filesystem layer, replacing the local driver with a network client — so everything above it works unmodified, and everything above it now has network latency underneath it. Object storage attaches *above* the filesystem, as an API the application calls directly; there is nothing to mount and nothing to format.
That single picture predicts most of the surprises. A database on object storage is slow not because object storage is slow but because the database wants to modify 8 KB pages in place and the contract only offers whole-object replacement. A shared filesystem feels like a local disk until an ls of a directory with 200,000 entries takes eleven seconds, because every entry is a network operation that a local disk answered from the page cache.
One system, three contracts, on purpose
Real systems use more than one, and the diagram should say which is which. The photo-sharing service below puts uploads in object storage because they are large, immutable and served to the public edge; puts the database on block storage because PostgreSQL needs random writes and a durable fsync; and gives the render workers a shared filesystem because the third-party rendering binary takes a directory path and cannot be changed.
That last one is the honest reason file storage exists in most architectures: not because it is the best contract, but because something in the stack demands a mount and rewriting it is not on the roadmap. That is a legitimate reason. It should just be written down as one, so nobody later assumes the shared filesystem was a design preference and adds three more workloads to it.
Key points
- Object, block and file are three access contracts, not three price tiers: the unit you address and the operations you get are what differ.
- Block storage attaches below the filesystem, file storage replaces it, object storage sits above it as an API — that layering predicts every performance surprise.
- One attachment, many attachments, or no attachment at all is usually the fastest way to narrow the choice.
- A well-designed system normally uses at least two of the three, and should say in the diagram why each one was chosen.
- File storage is often chosen because software demands a mount, not because it is the best contract. Write that down as the reason.
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.
- • Object: the application signs an HTTP request, the provider routes it by bucket and key, stores the bytes across replicated media and returns a version or entity tag.
- • Block: the hypervisor presents a virtual disk; the guest kernel's filesystem driver maps files to block ranges and issues reads and writes to the device queue.
- • File: the client mounts a network filesystem; every
open,stat,readandlockbecomes a protocol operation against the server, cached according to the protocol's consistency rules. - • The provider replicates the underlying media in all three cases — the durability guarantee is theirs, the correctness of what you write is yours.
- • Decide and record the contract per data class, not per application. "User uploads" and "the database" are different classes even inside one service.
- • Own capacity for block and throughput for file; object storage is the only one of the three you do not size in advance.
- • Own lifecycle: object storage grows forever unless someone writes a rule. See Storage Lifecycle: Hot, Warm, Archive, Delete.
- • Own the consistency assumptions your code makes — especially any code that lists a directory or a key prefix and assumes it is complete.
- • A database on a network filesystem: correct, and then catastrophically slow or corrupt when the protocol's locking semantics do not match what the engine assumed.
- • A full block volume — the database stops accepting writes while the instance, the load balancer and the health check all stay green.
- • A stalled network filesystem mount: every process that touches the path blocks in uninterruptible I/O, and the box becomes unkillable rather than unhealthy.
- • Object storage used as a work queue: listing a prefix is not a queue read, and two workers happily pick up the same key.
- • Object storage scales capacity without you; what runs out first is request rate against a hot key prefix, and the fix is key naming, not more storage.
- • Block storage scales by provisioning bigger or faster volumes — a vertical move with a ceiling, and IOPS usually runs out before capacity.
- • File storage scales capacity elastically but throughput is provisioned, and metadata operations are the dimension that saturates first.
- • The dimension that actually runs out at scale is rarely bytes. It is operations per second and the latency of each one.
- • Object storage is reachable from anywhere with credentials — its trust boundary is identity, not network position, which is why a misconfigured bucket is a headline and a misconfigured volume is a ticket.
- • Block volumes inherit the instance's exposure: whoever can reach the instance can read the filesystem on it.
- • File storage is reachable by every machine that can mount it, so its blast radius is the whole mount group — the set of machines is the access-control decision.
- • Encryption at rest is a default on all three at most providers; the meaningful question is who holds the key and who can decrypt. See Key Management and Encryption at Rest.
- • Object: pay for stored gigabytes, requests, and egress — the request meter surprises workloads with many small objects.
- • Block: pay for provisioned capacity whether or not you use it, plus provisioned IOPS or throughput where offered. An idle volume attached to a stopped instance still bills.
- • File: pay for capacity plus provisioned throughput, and it is usually the most expensive per gigabyte of the three.
- • The cheapest contract is usually the one that matches the access pattern, because a mismatch is paid in request counts, not in gigabytes.
- • Object: request rate, 4xx/5xx by operation, and throttling responses on a prefix.
- • Block: queue depth, average and p99 device latency, and free space — free space is the one that turns into an outage without warning.
- • File: metadata operations per second and client-side latency, split by mount.
- • The signal that lies: total bytes stored. It looks calm through every one of the failures above.
- • The local disk that came with the instance. For a cache, a scratch directory or a build workspace, ephemeral local storage is faster, simpler and free — and losing it on restart is fine by definition.
- • The database you already run. A few thousand small records with transactional requirements belong in PostgreSQL, not in a bucket with a naming convention.
- • No storage at all: recompute it. A derived thumbnail or an aggregate that costs 40 ms to regenerate does not always need a durable home and a lifecycle policy.
- • One contract instead of three. A small system that puts uploads in object storage and everything else in a managed database is a complete, defensible design — see No Cargo-Cult Infrastructure.
- • Object storage buys unbounded, cheap, globally reachable capacity; it costs you transactions, joins, in-place updates and predictable listing.
- • Block storage buys speed and full filesystem semantics; it costs you a provisioned, single-attach resource that you must size, monitor and grow.
- • File storage buys sharing without changing the application; it costs per-operation latency, a shared blast radius, and the highest price per gigabyte.
- • Using all three is correct and adds three operational surfaces — backup, capacity, access control and cost each need an owner per contract.
What people believe, and what is true
Object storage is just a cheaper disk.
It is a different contract. No random writes, no filesystem, no directory semantics, and a network round trip per operation — none of which a disk imposes.
A shared filesystem is the simple answer when two machines need the same data.
It is the answer that requires no code change. It also makes both machines share a failure domain and a latency profile, and it is the most expensive per gigabyte.
Storage choice is a cost decision.
It is an access-pattern decision that has cost consequences. Systems that optimize the price per gigabyte first end up paying for it in request charges and rewrites.