Back to Projects

FlashKV.

A High-Performance, Redis-Compatible In-Memory Database Built from Scratch in Rust

RUST

Why build a database from scratch?

FlashKV is a Redis-compatible, in-memory key-value database written in Rust — no framework doing the heavy lifting, no existing storage engine underneath. It speaks the real Redis wire protocol (RESP), so redis-cli, Telnet, and any Redis client library can connect to it without knowing the difference.

I built it because tutorials stop exactly where databases get interesting. I wanted first-hand answers to four questions:

  • How does one server handle thousands of concurrent connections without a thread per client?
  • How do wire protocols stay fast — and what does “zero-copy parsing” actually take to implement?
  • How do you make a shared data structure thread-safe without funnelling every request through one lock?
  • How does key expiry work when you can never afford to scan everything?

The result is a working server with 46 Redis commands across strings, lists, key management, and server operations, a 69-test suite, Criterion benchmarks — and a 15-part documentation series in the repo explaining every design decision, written so someone else could rebuild the project from it.

Architecture: three layers and a background sweeper

A request flows through three layers. A Tokio-based TCP listener accepts connections and spawns one async task per client. Each connection handler owns a buffered read loop that feeds the protocol parser. Parsed commands are dispatched by the command handler into the storage engine.

TCP Listener ---> Connection Handler ---> Command Handler ---> StorageEngine
  (Tokio)          (per-client              (46 commands)       (64 shards)
                    read loop)                                       ^
                                                                     |
                                                              ExpirySweeper
                                                            (background task)

The storage engine is where the interesting concurrency lives. Instead of one big lock, the keyspace is split across 64 shards — each shard is its own RwLock-protected HashMap, and keys are distributed by hash. Multiple readers proceed in parallel, and a writer only ever locks 1/64th of the keyspace, so under concurrent load most operations never contend at all.

Two smaller decisions that paid off:

  • Strings and lists are stored separately per shard — type confusion (running a string operation on a list) becomes impossible by construction rather than by runtime checks.
  • Lists are VecDeques — O(1) push and pop at both ends, which is exactly the access pattern of LPUSH / RPUSH / LPOP / RPOP.

Zero-copy protocol parsing

RESP — the Redis Serialization Protocol — is easy to read and tricky to parse well. Bytes arrive in arbitrary TCP fragments, so the parser has to handle partial input, resume when more data lands, and do it all without copying payloads around.

The parser is built on bytes::Bytes: bulk strings become cheap reference-counted slices into the read buffer instead of fresh allocations, keeping the hot path allocation-free. It supports all five RESP types (simple strings, errors, integers, bulk strings, arrays) plus inline commands — so you can literally Telnet in and type commands as plain text:

$ telnet localhost 6379
set name Ariz
+OK
get name
$4
Ariz

Protocol compatibility turned out to be the highest-leverage decision in the project: because FlashKV speaks real RESP, it inherits Redis’s entire tooling ecosystem — redis-cli, redis-benchmark, client libraries in every language — for free.

Expiry, testing, and what stuck with me

TTL support uses the same dual strategy Redis itself uses. Lazy expiry checks a key’s deadline on access — effectively free, but it never reclaims memory for keys nobody touches. So a background expiry sweeper scans shards for expired keys on an adaptive interval (100ms to 1s depending on load) and shuts down gracefully with the server.

Correctness is covered by 69 tests across the parser, storage engine, and command handlers, with Criterion benchmarks for the hot paths — and since the protocol is Redis-compatible, redis-benchmark runs against it out of the box.

What stuck with me most wasn’t any single feature — it was watching Rust’s ownership model turn whole classes of concurrency mistakes into compile errors: the sharded Arc<RwLock> design, parser lifetimes tied to network buffers, graceful shutdown of background tasks. Every one of those decisions is written up in the repo’s 15-part documentation series, from Rust fundamentals through benchmarking.

Back to Projects