Brief
You are handed a fleet of services that all want the same thing: a fast, shared, in-memory key-value store with TTLs, reachable over TCP by the standard Redis client libraries already in use. Real Redis is off the table — you are building the server yourself. Anything that speaks RESP must be able to connect to your port and get correct answers, under many simultaneous clients, inside a fixed memory budget.
Instructions
1. The wire protocol (RESP2)
Implement the Redis serialization protocol, version 2, as a standalone encode/decode
library with its own unit tests — no sockets involved yet. Every frame ends in \r\n
(CRLF); the first byte selects the type:
| First byte | Type | Framing |
|---|---|---|
+ |
Simple string | +OK\r\n — the text up to CRLF; may not contain CR or LF. |
- |
Error | -ERR unknown command\r\n — same framing as simple strings; the first word is conventionally an error code. |
: |
Integer | :1000\r\n — signed base-10 integer as ASCII text. |
$ |
Bulk string | $5\r\nhello\r\n — byte length, CRLF, then exactly that many raw bytes (binary-safe, may contain CRLF), then a trailing CRLF. $0\r\n\r\n is the empty string. $-1\r\n is the null bulk string (how GET reports a missing key). |
* |
Array | *2\r\n$4\r\nECHO\r\n$3\r\nhey\r\n — element count, CRLF, then that many complete frames of any type, nested arrays allowed. *0\r\n is empty; *-1\r\n is the null array. |
Clients send every command as an array of bulk strings (command name first, then arguments); your server replies with whichever type the command calls for. The parser must handle a frame split across multiple TCP reads and two commands arriving in one read — buffer until a complete frame is available.
2. The server and command set
Serve TCP on a configurable port (default 6379). Command names are case-insensitive. Support:
PING→+PONG;PING <msg>→ the message as a bulk string.ECHO <msg>→ the message as a bulk string.SET <key> <value>→+OK. Options:EX <seconds>/PX <milliseconds>attach a time-to-live; a plainSETon a key with a TTL clears that TTL.GET <key>→ the value as a bulk string, or null bulk string if absent or expired.DEL <key> [key …]→ integer count of keys actually removed.EXISTS <key> [key …]→ integer count of keys present.EXPIRE <key> <seconds>→:1if the TTL was set,:0if the key doesn't exist.TTL <key>→ remaining seconds;:-1for a key with no TTL,:-2for a missing key.INCR <key>/DECR <key>→ the new value as an integer; a missing key starts at 0; a value that isn't a valid integer returns an error, not a crash.
Unknown commands and wrong arity return -ERR … replies; a protocol error may close the
connection, a command error never does.
3. Concurrency model — a decision, not a default
Before milestone 3, choose and write down (in the project README) one of:
- Event loop — single thread, non-blocking sockets, readiness-based multiplexing (epoll/kqueue or your runtime's async layer). No locks; the store is touched by one thread only.
- Thread-per-connection over a locked store — blocking I/O per client, shared dictionary guarded by a lock (or sharded locks).
The write-up must state what each model costs (context switches and lock contention vs.
single-core ceiling and head-of-line blocking) and why the chosen one fits this project.
Whichever you choose, INCR from N parallel clients must be atomic: N clients × M
increments always ends at exactly N×M.
4. Expiry semantics
Two mechanisms, both required:
- Lazy — every read path (
GET,EXISTS,TTL,INCR, …) checks the key's deadline first; an expired key is deleted on the spot and treated as absent. - Active — a periodic sweep (e.g. 10×/second) samples a bounded batch of keys that carry TTLs (Redis uses 20), deletes the expired ones, and immediately repeats while more than 25% of the sample was expired. This bounds memory held by dead keys that are never read again, without ever scanning the whole keyspace in one pass.
Store deadlines as absolute monotonic-clock instants, not remaining durations.
5. Eviction under maxmemory
Accept a maxmemory <bytes> configuration. When a write would push tracked memory past
the cap, evict keys until it fits, using approximate LRU: keep a last-access clock on
every entry, sample K random entries (K = 5 is fine), evict the least recently used of
the sample, repeat as needed. Exact LRU (a global linked list) is explicitly not
required — match Redis's trade: O(1) bookkeeping, statistically near-LRU outcomes. If
eviction cannot free enough space, answer writes with an -OOM error rather than
crashing.
6. Benchmark protocol
Measure with the stock tool: redis-benchmark -t SET,GET -n 100000 -c 50 against your
port (add -P 16 for a pipelined run). Record throughput and latency, capture a CPU
profile or flame graph under that load, fix the single largest cost you find, and re-run
the identical command. The deliverable is a before/after table plus one paragraph naming
the bottleneck and the fix. No harness of your own is needed unless redis-benchmark is
unavailable — in which case: 50 concurrent connections each issuing 2 000 alternating
SET/GET on random keys from a 10 000-key space, wall-clock timed, p50/p99 reported.
Examples
A SET/GET exchange on the wire:
client → *3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
server → +OK\r\n
client → *2\r\n$3\r\nGET\r\n$5\r\nhello\r\n
server → $5\r\nworld\r\n
client → *2\r\n$3\r\nGET\r\n$7\r\nmissing\r\n
server → $-1\r\n
SET with a 100 ms TTL, read before and after expiry:
SET k v PX 100 → +OK
GET k → $1\r\nv\r\n (immediately)
GET k → $-1\r\n (after 100 ms)
TTL k → :-2\r\n (key is gone)
Constraints
- Standard library only for networking and data structures; no Redis client/server libraries, no protocol parsers. (A test dependency to drive redis-cli is fine.)
- Values are binary-safe byte strings; never assume UTF-8.
- The store lives in memory; persistence is a stretch goal, not part of the subject.
- Correctness under concurrency beats raw speed everywhere except milestone 6, where you must report honest numbers on an idle machine and name the measurement conditions.
Acceptance
Mapped one-to-one onto the exercise's milestones:
- RESP parser — a unit-test suite covers all five types, null bulk string and null array, nested arrays, a frame split across two reads, and two frames in one read; a round-trip (decode → encode) reproduces every test vector byte-for-byte.
- PING/ECHO server — a stock
redis-cli -p <port> PINGprintsPONGandredis-cli ECHO heyprintshey, with no client-side flags or patches. - Concurrent clients — the README states the chosen model and its trade-offs; 10
parallel
redis-cliloops each runningINCR counter1 000 times leavecounterat exactly 10000. - GET/SET with expiry —
SET k v EX 1reads backvimmediately and null after 1 s; with active sweep enabled, a batch of 10 000 keys set withPX 100and never read again measurably shrinks the store within a few seconds. - Eviction — with
maxmemoryset below the working set, writing past the cap evicts instead of failing; a hot key touched between every insertion survives while cold keys go; memory stays under the cap for the whole run. - Benchmark & profile — a before/after table from the same
redis-benchmarkcommand, the profile artifact (flame graph or equivalent), and a paragraph naming the fixed bottleneck.
Related
- Build your own Redis — the exercise this is the subject of.
- Coding Challenges — Build your own Redis — the original challenge this statement is adapted from.
- Redis protocol specification — the canonical RESP definition restated in §1.