A small Redis-style in-memory key-value server written from scratch in Rust. It speaks RESP2 and supports a focused subset of Redis commands.
This is a personal portfolio project focused on the internals behind a Redis-style server: RESP parsing, non-blocking TCP I/O, command execution, key expiry, append-only persistence, memory accounting, and cache eviction policies.
It is intentionally smaller than Redis itself. The goal is to show a working systems project with a clear execution path, a tested protocol layer, and implementation choices that are easy to inspect.
- RESP2 parser and encoder for simple strings, errors, integers, bulk strings, null bulk strings, arrays, and null arrays.
- Non-blocking TCP server built with
mio. - Multiple client support with read and write buffering.
- Pipelined command handling.
- Binary-safe key and value storage.
- Core Redis-style commands:
PINGECHOSETSET key value EX secondsGETDELEXISTSEXPIRETTLINFO
- Lazy expiration when keys are read.
- Active expiration sampling on the server loop.
- Append-only file persistence with startup replay.
- Configurable AOF fsync behavior:
alwayseverysecno
- Memory accounting for stored values and expiry metadata.
- Eviction policy implementation hooks with support for:
noevictionallkeys-randomvolatile-randomvolatile-ttlallkeys-sieve
- SIEVE-style second-chance eviction for touched keys.
INFOoutput for server version, git hash, build type, memory usage, key count, expiry count, and eviction policy.mimallocas the global allocator.- Unit tests covering protocol parsing, command behavior, expiry, eviction, pipelining, and AOF replay.
Install a Rust toolchain that supports the 2024 edition, then run:
cargo test
cargo runBy default the server listens on 127.0.0.1:6379 and stores append-only persistence data in db.aof.
In another terminal, connect with redis-cli:
redis-cli -p 6379 PING
redis-cli -p 6379 SET greeting hello
redis-cli -p 6379 GET greeting
redis-cli -p 6379 SET session abc EX 10
redis-cli -p 6379 TTL session
redis-cli -p 6379 EXISTS greeting session missing
redis-cli -p 6379 DEL greeting
redis-cli -p 6379 INFOYou can also use any RESP-compatible TCP client. The server speaks RESP over a normal TCP socket.
Start the server with default settings:
cargo runListen on a different address:
cargo run -- --host 127.0.0.1 --port 6380Use a custom AOF file:
cargo run -- --aof-path ./data/db.aofUse everysec fsync behavior:
cargo run -- --aof-fsync-policy everysecAvailable CLI options:
Usage: redis [OPTIONS]
Options:
-p, --port <PORT> Port to listen for incoming connections [default: 6379]
--host <HOST> Host to listen for incoming connections [default: 127.0.0.1]
--aof-enabled Enable append-only file persistence
--aof-path <AOF_PATH> Path to the append-only file [default: db.aof]
--aof-fsync-policy <AOF_FSYNC_POLICY> When to flush AOF writes to disk [default: always] [possible values: always, everysec, no]
-h, --help Print help
-V, --version Print version
AOF is enabled by default in the current CLI configuration.
| Command | Example | Notes |
|---|---|---|
PING |
PING |
Returns PONG. |
PING message |
PING hello |
Echoes the provided message. |
ECHO message |
ECHO hello |
Returns the provided message. |
SET key value |
SET name sazid |
Stores a binary-safe value. A plain SET clears any existing TTL. |
SET key value EX seconds |
SET token abc EX 30 |
Stores a value with a TTL. TTL must be greater than zero. |
GET key |
GET name |
Returns a bulk string or null bulk string. |
DEL key [key ...] |
DEL a b c |
Returns the number of deleted keys. |
EXISTS key [key ...] |
EXISTS a b c |
Returns the number of existing keys. |
EXPIRE key seconds |
EXPIRE token 30 |
Returns 1 when the key is updated, 0 when missing. Non-positive TTL deletes the key. |
TTL key |
TTL token |
Returns remaining seconds, -1 for no expiry, and -2 for missing keys. |
INFO |
INFO |
Returns server and memory metadata. |
Mutating commands that successfully change state are appended to the AOF:
SETSET ... EXDELwhen at least one key is deletedEXPIREwhen the target key exists
Read-only commands are not persisted.
The project is intentionally compact and organized around the major parts of a Redis-style server:
src/
main.rs CLI entry point
config.rs CLI flags and runtime config
resp.rs RESP2 parser and encoder
db.rs In-memory key-value database, expiry, memory accounting
eviction.rs Eviction policy selection and memory-limit enforcement
server/
mod.rs TCP event loop, clients, pipelining, AOF replay
aof.rs Append-only file writer and fsync policies
commands/
mod.rs Command dispatch
set.rs SET and SET EX
get.rs GET
del.rs DEL
exists.rs EXISTS
expire.rs EXPIRE
ttl.rs TTL
ping.rs PING
echo.rs ECHO
info.rs INFO
Request flow:
miopolls the server socket and client sockets.- Client bytes are appended to a per-client read buffer.
resp::decode_oneparses one complete RESP value at a time.- The command dispatcher validates arguments and applies the command to
RedisDb. - Successful mutating commands are written to the append-only file.
- Encoded RESP responses are queued in the client's write buffer.
- The event loop flushes pending responses when the socket is writable.
This design keeps networking, parsing, command dispatch, persistence, and storage mostly separate, which makes the code easier to test without needing a live TCP socket for every behavior.
The server uses append-only file persistence:
- On startup, the configured AOF file is read if it exists.
- Each command in the AOF is decoded as RESP and replayed into the in-memory database.
- During runtime, successful mutating commands are appended in RESP form.
- Fsync behavior is controlled by
--aof-fsync-policy.
The supported fsync policies are:
| Policy | Behavior |
|---|---|
always |
Sync after every appended command. Safest, slowest. |
everysec |
Sync roughly once per second. Balanced durability and throughput. |
no |
Let the operating system decide when data reaches disk. Fastest, least explicit durability. |
This is AOF-only persistence. There is no RDB snapshot format.
Key expiry uses two strategies:
- Lazy expiry: commands such as
GET,EXISTS, andTTLcheck whether a key has expired before returning a result. - Active expiry: the server loop periodically samples expiring keys and deletes stale entries.
The active expiry loop samples a bounded number of keys so expiry cleanup does not monopolize the event loop.
The database tracks an estimated memory usage for values and expiry metadata. When a memory limit is configured at the database layer, eviction can be enforced with several policies:
| Policy | Behavior |
|---|---|
noeviction |
Reject writes that would exceed the configured memory limit. |
allkeys-random |
Evict random keys from the full keyspace. |
volatile-random |
Evict random keys that have a TTL. |
volatile-ttl |
Evict keys with the shortest TTL, using sampling for larger expiry sets. |
allkeys-sieve |
Use a SIEVE-style second-chance scan over all keys. |
allkeys-sieve is the default database policy. Reads mark keys as touched, and the eviction hand gives touched keys one second chance before selecting a victim.
The current CLI does not yet expose maxmemory or eviction policy flags. Those paths are implemented and tested at the database layer and are natural next steps for runtime configuration.
Run the test suite:
cargo testThe suite currently covers:
- RESP encoding and decoding.
- Invalid and incomplete protocol input.
- Command argument validation.
- Case-insensitive command dispatch.
- Key/value mutation and lookup.
- TTL behavior and lazy expiry.
- Active expiry sampling.
- AOF replay and malformed AOF handling.
- Pipelined command processing.
- Memory accounting.
- Eviction policies and out-of-memory responses.
At the time this README was written, the test suite contains 168 tests.
This project is meant to be read as much as it is meant to be run. It demonstrates:
- Implementing a network protocol parser without relying on Redis internals.
- Building a non-blocking event loop around
mio. - Handling partial reads, buffered writes, and pipelined requests.
- Modeling Redis-style command semantics in small, focused modules.
- Replaying an append-only log into an in-memory state machine.
- Tracking approximate memory usage and enforcing eviction policies.
- Writing tests around protocol, storage, persistence, and server behavior.
This is not a production replacement for Redis. It is a focused educational and portfolio implementation.
Notable limitations:
- Supports only a small command subset.
- Single-process, single-threaded server loop.
- No replication.
- No clustering.
- No transactions.
- No Lua scripting.
- No pub/sub.
- No streams, sets, hashes, or sorted sets.
- No authentication or ACL system.
- No TLS.
- No RDB snapshots.
- No benchmark suite yet.
Potential next steps:
- Expose
maxmemoryand eviction policy through CLI flags. - Add integration tests that drive the server over TCP with
redis-clior a RESP client. - Add benchmarks for parser throughput, command latency, AOF fsync modes, and eviction behavior.
- Implement additional commands such as
MGET,MSET,INCR,DECR, andFLUSHDB. - Add a clean shutdown path.
- Add GitHub Actions for formatting, clippy, and tests.
- Add Docker packaging for quick demos.
The server is usable for local experimentation and for demonstrating the core mechanics of a Redis-style database server. It is best treated as a learning project and portfolio artifact rather than infrastructure.