KnowSys
◈Build it yourself

Build a Redis clone

A single-threaded server that speaks RESP to the real redis-cli, holds strings, lists, hashes and sorted sets, expires keys, persists to disk with snapshots and an append-only file, and streams its writes to a replica.

Intermediate⏱ 4–7 weekendsGo · Rust · C · Python

Redis handles a huge number of requests per second on one core, and it does it without locks, threads or anything clever in the hot path. It reads bytes off sockets, parses a very simple protocol, touches a hash table in memory, and writes bytes back. The speed comes from never waiting.

You'll build a server that the real redis-cli and redis-benchmark can talk to. It starts as a loop that answers PING, and grows into a store with several data types, key expiry, two kinds of persistence and a replica that follows the primary. Along the way you'll write an event loop by hand. Most engineers have probably used one for years without ever seeing it.

01Why build this

Redis sits in front of a large share of production databases, and it fails in ways that make much more sense once you've built one:

  • Event loops stop being abstract. Node, nginx, Netty and Redis all run on the same idea: one thread, non-blocking sockets and epoll or kqueue. You'll have written one.
  • Latency spikes get an explanation. A single slow KEYS * blocks every client, because there's only one thread. You'll see it happen in your own server.
  • "Is Redis durable?" gets a precise answer. You'll know what appendfsync everysec can lose and why a BGSAVE doubles memory on a busy primary.
  • Replication lag becomes concrete. You'll know what the replica has, what it's missing, and how it catches up after a disconnect.

It's also a pretty friendly first network server. Every milestone produces something you can poke at with redis-cli.

02What you're building

One command's trip through the finished server:

What your server does with `SET user:1 ada EX 60`
●
⇄
Socket
readable
↻
Event loop
epoll / kqueue
⌗
Parser
RESP
▦
Keyspace
hash table
✎
AOF
append
⇉
Replica
stream
Step 1. The client writes *5\r\n$3\r\nSET\r\n... to its TCP connection. The bytes sit in the kernel's receive buffer.
1 / 6

?Why can one thread be fast enough?

Because each command does very little work, all of it in memory. The expensive part of a request is waiting for the network, and an event loop never waits: it asks the kernel which sockets are ready and handles only those. Adding threads would add locks around the keyspace and buy almost nothing until the network itself is saturated.

03Before you start

You needWhyWhere to get it
The real redis-cli and redis-benchmarkYour test client and load generatorInstall Redis from your package manager; don't run its server
The RESP specificationEvery byte on the wireredis.io, under "Redis serialization protocol specification"
Sockets: socket, bind, listen, acceptYou'll manage connections yourselfChapter 10
Non-blocking I/O and epoll or kqueueThe core of milestone 2man 7 epoll, or man 2 kqueue on macOS
One language with access to raw socketsGo and Rust are easiest; C matches Redis's own sourceAny

04The roadmap

Eight milestones. Each one works with the real client, so you can test with a tool you didn't write.

1

Answer PING

1 evening

Listen on port 6379, accept a connection, and read what the client sends. redis-cli sends every command as a RESP array of bulk strings: an asterisk and a count, then each argument as a length-prefixed string.

Write a parser for that shape and reply with +PONG\r\n. Keep the parser incremental from day one: it should be able to say "not enough bytes yet" without throwing away what it has.

You’ll learnTCP serverRESP simple stringsarrays of bulk strings
Done when: redis-cli PING prints PONG and redis-cli ECHO hi prints hi.
2

Many clients, one thread

1 weekend

Make the listening socket and every client socket non-blocking. Register them with epoll (or kqueue), and loop: wait for events, accept new clients, read from readable ones, and write pending output to writable ones.

In Go it's tempting to start a goroutine per connection instead. That works, but then you need a lock around the keyspace. Build the single-threaded loop at least once. It's the design decision the rest of Redis is built around.

You’ll learnnon-blocking socketsepoll / kqueuereadiness vs completionoutput buffers
Done when: Fifty redis-cli sessions stay connected at once, and redis-benchmark -t ping finishes without errors.
3

Strings and a keyspace

1 evening

Add a hash table from key to value and implement GET, SET, DEL, EXISTS and INCR. Return the right reply types: bulk strings, integers, nil, and -ERR messages that match what real Redis says closely enough for clients to parse.

Run redis-benchmark with pipelining. If you handled partial reads properly in milestone 2, it works. If you didn't, this is where it breaks.

You’ll learnGET / SET / DELINCRRESP errors and nilpipelining
Done when: redis-benchmark -t set,get -P 16 runs clean, and INCR on a non-integer returns an error reply.
4

Expiry

1 weekend

Keep a second table from key to expiry time. Check it on every access and delete the key if it's past due. That's lazy expiry, and it's enough for correctness.

It isn't enough for memory: keys nobody reads again never get deleted. Add a periodic task, run from the event loop, that samples a few keys with a TTL and deletes the expired ones, repeating while a large share of the sample was expired. Redis does roughly this, trading perfect cleanup for bounded work per tick.

You’ll learnTTLlazy expiryactive expiry samplingmonotonic clocks
Done when: A key set with EX 1 is gone after a second, and a million expired keys are cleaned up without a client touching them.
5

Lists, hashes, sets and sorted sets

1–2 weekends

Make values tagged by type and implement LPUSH/LRANGE, HSET/HGET, SADD/SMEMBERS, and the sorted set commands. Using a key with the wrong command returns WRONGTYPE.

Sorted sets are the interesting one. Redis pairs a hash table (member to score) with a skip list ordered by score, so it can both look up a member and walk a range quickly. Write the skip list yourself. It's short, a bit fiddly at first, and it's a data structure you'll recognise in other systems.

You’ll learntype errorsskip listsO(log n) rankingWRONGTYPE
Done when: ZADD, ZRANGE ... WITHSCORES and ZRANK agree with real Redis on the same inputs.
6

Snapshots: RDB

1 weekend

Serialise the keyspace to a file with your own compact format. SAVE does it in the loop and blocks everyone. BGSAVE calls fork(): the child writes the snapshot while the parent keeps serving, and the kernel's copy-on-write pages give the child a frozen view for free.

Write to a temp file and rename it into place. Then watch memory while you write heavily during a BGSAVE. Every page the parent touches gets copied. A busy primary can need a lot more RAM during a snapshot for that reason.

You’ll learnfork()copy-on-writepoint-in-time snapshotsatomic rename
Done when: SAVE and BGSAVE write a file, a restart loads it, and clients keep getting replies while BGSAVE runs.
7

The append-only file

1 weekend

Append every write command, in RESP, to a log file. On startup, replay it. Offer the same three policies Redis does: fsync after every write, once per second, or never and let the OS decide. Measure the throughput of each.

The log grows forever, so add a rewrite: fork, write the smallest set of commands that rebuilds the current keyspace, and append whatever changed meanwhile before swapping files. Handle a half-written last command on replay.

You’ll learnAOFfsync policieslog rewritingtruncated tails
Done when: With appendfsync always, kill -9 loses no acknowledged write, and BGREWRITEAOF shrinks the file without losing data.
8

A replica

1–2 weekends

When a replica connects, the primary sends a full snapshot and then streams every write command after it. Track a replication offset: the number of bytes of command stream sent. The replica reports how far it got.

Keep a fixed-size backlog of recent commands on the primary. A replica that reconnects with an offset still inside the backlog gets only the missing part, not a new snapshot. Then kill the primary right after a write and notice that the replica may never have seen it. That's asynchronous replication.

You’ll learnPSYNCreplication offsetbacklogasynchronous replication
Done when: A second instance started with REPLICAOF receives every write, and after a brief disconnect it catches up without a full resync.

05Traps that catch everyone

SymptomCauseFix
Works in redis-cli, fails under redis-benchmarkYou assumed one read is one commandBuffer per client; parse in a loop until the buffer holds no complete command
Large replies get cut offA non-blocking write sent only part of the bufferKeep the rest in the output buffer and wait for writability
The server stalls for everyoneA slow command, or a blocking disk write, inside the event loopKeep O(n) work bounded; move disk flushing to a background thread or child
Memory keeps growing with expired keysOnly lazy expiryAdd the sampling expiry cycle
TTLs jump after an NTP adjustmentWall-clock time used for relative expiriesUse a monotonic clock for timers; store absolute Unix times only for persistence
RDB file is corrupt after a crashSnapshot written in placeWrite to a temp file, fsync, rename
Replica diverges after reconnectingOffsets counted in commands on one side and bytes on the otherCount bytes of the RESP stream everywhere

06Stretch goals

  • Pub/sub. SUBSCRIBE and PUBLISH, where a subscribed connection changes mode and receives pushed messages.
  • Transactions. MULTI/EXEC and WATCH: optimistic concurrency in about a page of code.
  • Blocking commands. BLPOP parks a client until a list gets an element, without blocking the loop.
  • Eviction. A maxmemory limit with approximated LRU or LFU, the way Redis samples instead of keeping an exact order.
  • Cluster mode. Hash slots, MOVED redirects, and a client that follows them. Chapter 29 covers partitioning.

07References worth your time

Redis serialization protocol specification

The RESP spec on redis.io. Short, precise, and the only document you need for milestone 1.

James Smith, Build Your Own Redis with C/C++

A free online book that builds a Redis-like server from sockets up, including an event loop, a hash table and a sorted set.

CodeCrafters, Build your own Redis

A staged challenge with automated tests for each step, including RDB loading and replication. Useful as a test suite even if you follow this roadmap.

Redis source: ae.c, t_zset.c, replication.c

The event loop, the sorted set and replication, in readable C. Look at each one after the matching milestone.

Redis persistence and replication docs

The official explanation of RDB, AOF, fsync policies, PSYNC and the backlog, with the trade-offs stated plainly.

William Pugh, Skip Lists

The 1990 paper that introduced the structure behind sorted sets. A few pages long and easy to implement from.

Chapters that back this project

Next project⇅ a TCP/IP stack in userspace→