All storage is not the same. Block storage presents a disk you can mount and read at any byte; object storage presents a keyed, addressable store of objects with metadata. Choosing correctly is about the access pattern, not the buzzword.
Block storage is a byte-addressable disk
Block storage is a raw, byte-addressable disk: a volume is handed to a host or container as a device with a filesystem, and the guest owns low-level blocks, page cache, and filesystem. Because it behaves like a local disk, block storage suits databases, filesystems, and anything that needs low latency, byte-level writes, and file locking. Its weakness is elasticity and sharing: a block volume typically binds to one writer at a time, is not a natural fit for ‘millions of files read by many clients’, and generous sizes cost the same whether you use them or not. If your workload is a database or a VM, you are almost always asking for block.
Object storage is web-scale key-value blobs
Object storage is a web-scale key-value store for blobs: you address content by a flat, global key (a bucket name plus an object key), tens of thousands of clients hit it concurrently, and the API (GET/PUT/DELETE) makes it a natural fit for media, backups, and data lakes. Its latency is higher and its model is ‘write whole object, read whole object’, so it is the wrong shape for byte-for-byte OLTP mutations. The cool thing is that it is designed to be shared across unlimited readers, so the cost model and access pattern both favor storing the durable, big, rarely-mutated files here.
Match storage to the access pattern
Match storage to the access pattern, not to brand loyalty. If you need to append bytes, lock rows, run SQL, or mount a filesystem — block. If you need to store an archive, a log snapshot, or user-uploaded media that is written once and read many times, and you want it across many machines — object. The two are complements, not rivals: the common architecture runs block for hot, mutable data and object for cold, durable assets, and moves data between them intentionally. Choosing by how you read and write, not by what you already have, is what keeps the bill and the latency honest.
When it breaks
Treating object storage like a block device fails first. A database file or journal placed directly on a keyed object cannot take random byte writes or file locks: every small update becomes a full-object read-modify-write, concurrent writers silently replace each other, and a crash between download and re-upload leaves a truncated replacement instead of a recoverable page. Latency also betrays the mismatch, because a one-byte change still pays the cost of shipping the whole object.
The second failure is a million tiny objects. Each PUT, GET, and especially LIST is a billed API call with per-request latency, and a flat namespace with no real directories means enumerating one prefix can page through thousands of keys. A workload that writes one object per event and then lists the bucket to find work turns metadata into the bottleneck. The third failure is subtler: read-after-write visibility. A reader can fetch the previous version inside the consistency window, so two processes disagree about which object is current until the new version propagates. The fourth is lifecycle rules: one overly broad prefix or expiration policy quietly deletes backups, and without versioning that delete is final.
Check it quickly
Save the following as storage_semantics.py and run python3 storage_semantics.py. It uses only the standard library, touches no network or disk, and keeps all state in two in-memory objects. The block fixture supports in-place overwrites; the object fixture stores whole immutable values in a dict with a monotonically increasing version.
"""Block in-place overwrite vs object whole-object replace + versioning."""
class BlockVolume:
def __init__(self, data: bytes):
self._disk = bytearray(data)
def write(self, offset: int, chunk: bytes) -> None:
self._disk[offset:offset + len(chunk)] = chunk
def read(self) -> bytes:
return bytes(self._disk)
class ObjectStore:
"""Minimal object semantics: whole-value PUT, version per key."""
def __init__(self):
self._objects: dict[str, tuple[bytes, int]] = {}
def put(self, key: str, data: bytes) -> int:
version = self._objects[key][1] + 1 if key in self._objects else 1
self._objects[key] = (bytes(data), version)
return version
def get(self, key: str) -> tuple[bytes, int]:
return self._objects[key]
def main() -> None:
# 1. Block: in-place overwrite keeps neighbours.
block = BlockVolume(b"ABCDEF")
block.write(2, b"XX")
assert block.read() == b"ABXXEF", block.read()
print(f"block: {block.read()!r} preserved neighbours")
# 2. Object: PUT replaces the whole value and bumps the version.
store = ObjectStore()
v1 = store.put("report", b"ABCDEF")
old_data, old_version = store.get("report")
v2 = store.put("report", b"XYZ")
new_data, new_version = store.get("report")
assert (v1, v2) == (1, 2)
assert len(old_data) == 6 and len(new_data) == 3
assert old_version == 1 and new_version == 2
assert new_data == b"XYZ", "object replace must not merge with prior bytes"
print(f"object: v{v1} -> v{v2} len {len(old_data)} -> {len(new_data)}")
# 3. Consistency window: a stale reader still holds v1.
stale = (old_data, old_version)
current = store.get("report")
assert stale[1] != current[1], "versions must expose staleness"
print(f"stale reader saw v{stale[1]}, current is v{current[1]}")
# 4. No partial update: object API has no offset write.
assert not hasattr(store, "write"), "object store must not offer block writes"
print("PASS: block mutates in place, objects replace by version")
if __name__ == "__main__":
main()
The assertions isolate the semantic gap. The block write changes two bytes while neighbours stay identical, which proves in-place mutation. The object replacement changes the length and bumps the version from one to two, which proves whole-object replacement with explicit history. The stale-version check reproduces the consistency window: a reader holding version one disagrees with version two. There is no partial object update without a read-modify-write cycle.
Expected output:
block: b'ABXXEF' preserved neighbours
object: v1 -> v2 len 6 -> 3
stale reader saw v1, current is v2
PASS: block mutates in place, objects replace by version
The run passed with Python 3.12 on macOS. It models semantics only, not latency, durability, pricing, or real consistency behavior. Before choosing storage, repeat the access-pattern question against production-sized objects and a real client library.
Further reading
Sources
- AWS: Amazon S3, object storage for scale, checked 2027-03-14.
- Google Cloud: Cloud Storage, object storage for durable workloads, checked 2027-03-14.
- Backblaze: B2 object storage, object storage for durable data, checked 2027-03-14.