The short version, for whichever hat you're wearing
- Split files into chunks, copy each chunk three times, and put the copies in different places.
- Every change gets a version; every delete leaves a tombstone; every chunk carries a checksum.
- Assume machines vanish: notice fast, route around them, and repair in the background.
- TCP is a byte stream: length-prefix your messages, never search for an end marker.
- Writes need a compare-and-set (an ETag and If-Match), or two uploads silently overwrite each other.
- Pick R + W > N, and every read overlaps the latest write.
- Consistent hashing means adding a server moves about 1/n of the data, not most of it.
- Small metadata, big data: names and versions in a Raft group; bytes on replicated or erasure-coded storage.
- Gossip for membership, Merkle trees for repair, zones for blast radius.
- Durability is repair speed: the risk window is the time a chunk spends under-replicated.
- Watch under-replicated chunks, p99 and disk, and practise killing a node.
- Erasure coding halves storage cost for cold data, at the price of slower repair.
What we built
Four laptops on home Wi-Fi, one shared folder. Press play.
What it measured in 2020
| operation | 1 KB file | under 1 MB |
|---|---|---|
| create + replicate | 0.4 s | 0.7 s |
| download, edit, upload | 1 s | 5 s |
| upload to every replica | 0.7 s | 4 s |
real numbers from our report: 4 servers, 4 clients, home network, rounded to the second
It worked, and it survived crashed laptops. It also had every classic distributed-systems bug in miniature. Let's fix them one at a time.
TCP sends bytes, not messages
We ended every message with the word aosppdfs and read 1,024 bytes at a time. Watch what can go wrong:
import struct
async def send(writer, payload: bytes):
writer.write(struct.pack("!I", len(payload)) + payload) # 4-byte length, then the bytes
await writer.drain() # backpressure: wait if the socket is full
async def recv(reader) -> bytes:
(size,) = struct.unpack("!I", await reader.readexactly(4))
if size > MAX_FRAME:
raise ValueError("frame too large") # never trust a length blindly
return await reader.readexactly(size) # exactly that many, however TCP splits themOne lock for everything
Every thread shared one global directory behind one lock. Four reads and two writes arrive:
Snapshots: readers grab the current immutable map; a writer builds a new one and swaps the pointer. At thousands of connections, swap threads for asyncio streams.
Where does a file live?
We picked a random peer for the copy, then broadcast the whole directory so everyone could find it. Hashing makes the location computable instead:
Real 32-bit hashes of 120 file names, 16 virtual points per server. Consistent hashing moves about 1/n of the files; mod n moves most of them.
How many copies must say yes?
Keep N = 3 copies. A write waits for W of them; a read asks R of them. Break some servers and try it:
async def put(name, data, if_match, W=2):
chunks = [data[i:i + CHUNK] for i in range(0, len(data), CHUNK)] # 4 MiB pieces
ids = [sha256(c) for c in chunks] # the checksum is the chunk's name
replicas = ring.owners(name, n=3, distinct_zones=True)
acks = await first(W, [r.store(ids, chunks) for r in replicas], timeout=0.5)
if len(acks) < W:
raise Unavailable("not enough copies, try again") # never claim it was saved
return await metadata.compare_and_set( # Raft: one truth per name
name, expect=if_match, new={"chunks": ids, "version": if_match + 1})Two people, one file
On the web this is just ETag + If-Match. For offline edits, vector clocks spot concurrent versions.
Deleting is a write
Who's still alive?
Our peer list was typed by hand. Real clusters gossip: every second, each server tells a couple of random others what it knows.
Missed heartbeats make a server suspect; gossip spreads the verdict in about log(n) rounds. See gossip protocols.
Is my copy still right?
Disks rot quietly. Hash every chunk, then hash the hashes into a tree, and two replicas can find a difference without shipping the data:
Copies, or math?
Erasure codes rebuild lost pieces from any k survivors (Reed–Solomon). Hot files: copies. Cold files: math.
Agreeing on names
Bytes can live anywhere. But "which version of notes.txt is current?" needs one answer. That's consensus:
Raft picks consistency over availability: a minority can't write. That's the C in CAP, chosen on purpose.
Fast downloads
From one laptop to the internet
Same ideas at every step. Press play, and it grows from the bottom up.
PUT /files/notes.txt If-Match: "v3" → 200 ETag: "v4" | 412 someone beat you
GET /files/notes.txt Range: bytes=0-4194303 → 206 the first chunk
GET /files/notes.txt If-None-Match: "v4" → 304 your copy is current
DELETE /files/notes.txt If-Match: "v4" → 204 and a tombstone
GET /health → 200 for the load balancerAdding a server
Watching it heal
The number that matters most isn't latency. It's how many chunks are missing a copy right now.
Leslie Lamport: "A distributed system is one in which the failure of a computer you didn't even know existed can render your own computer unusable." Our 2020 answer was a list of every computer, typed by hand.
2020 vs today, one page
- Messages on a TCP streamend marker (2020)length prefix
Any bytes, no hangs. Cost: a 4-byte header.
- Many connectionsthread each + one lock (2020)asyncio + snapshots
Readers never wait. Cost: async code everywhere.
- Where a file livesrandom peer (2020)hash ring, zone-aware
Computable, and growth moves 1/n. Cost: virtual nodes to balance.
- How many copies2 (2020)3, W = 2, R = 2
Survives one loss during repair. Cost: 3× storage.
- Two writerslast writer wins (2020)compare-and-set
No silent loss. Cost: clients retry.
- Deletesrename to "name *" (2020)versioned tombstones + GC
We were right; add expiry. Cost: tombstones take space.
- Membershiptyped IP list (2020)seeds + gossip
Servers come and go. Cost: tuning suspicion timeouts.
- Metadatabroadcast to all (2020)a Raft group
One truth per name. Cost: a majority must be up.
- Bit rottrust the disk (2020)checksums + Merkle repair
Corruption is found, not served. Cost: hashing CPU.
- Getting inraw sockets on Wi-Fi (2020)HTTPS, tokens, a load balancer
Safe to expose. Cost: one more hop.
The well-earned drink
Every Brew Miles trip ends with one:
- Frame your bytes, and version everything, deletes included.
- Hash to place, copy to survive, and spread copies across failure zones.
- Make writes conditional and pick quorums that overlap.
- Gossip to notice, Raft to agree, Merkle trees to repair.
- Watch under-replication, and kill a server on purpose before one dies on its own.
Show the papers and docs this post leans on
- Dynamo (2007): consistent hashing, quorums, vector clocks, hinted handoff, Merkle trees.
- The Google File System (2003) and the HDFS design: chunks, a metadata master, rack-aware copies.
- In Search of an Understandable Consensus Algorithm (Raft).
- Time, Clocks, and the Ordering of Events (Lamport, 1978).
- SWIM (IEEE DSN 2002): gossip-based membership and failure detection.
- The Tail at Scale: hedged requests.
- Ceph erasure coding and Cassandra repair in practice.
- asyncio streams (Python docs), for the laptop version.