Radish is a lightweight, blazing-fast, in-memory key-value data store written in Rust. It implements a faithful subset of the REdis Serialization Protocol (RESP) and mirrors the single-threaded, asynchronous concurrency model that makes Redis itself so performant β all without a single Mutex in sight.
Built as a learning project to deeply understand Redis internals, async Rust, and network protocol design from scratch.
| Command | Syntax | Description |
|---|---|---|
| SET | SET key value [EX seconds | PX milliseconds] |
Store a value under a key, with optional TTL |
| GET | GET key |
Retrieve the value for a key (returns nil if missing or expired) |
| TTL | TTL key |
Query remaining time-to-live of a key (in seconds) |
- Attach expiry durations to any key via
EX(seconds) orPX(milliseconds) onSET. TTLreports remaining seconds,-1for keys with no expiry, and-2for non-existent/expired keys.- Lazy expiration: expired keys become invisible on access β no background threads or timers required.
| Command | Syntax | Description |
|---|---|---|
| PING | PING [message] |
Returns PONG or echoes the argument back |
| ECHO | ECHO message |
Echoes the given message back to the client |
- Fully compatible with
redis-cliand any RESP-speaking client. - Parses Simple Strings (
+), Bulk Strings ($), Arrays (*), Integers (:), and Errors (-) natively. - Returns proper RESP-encoded responses: simple strings, bulk strings, null bulk strings, integers, and errors.
- Gracefully handles unknown commands with informative error messages.
Radish deliberately avoids multi-threaded complexity. Like the original Redis, it runs an event loop on a single thread, handling thousands of concurrent connections through asynchronous I/O.
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tokio (current_thread) β
β βββββββββββββββββββββββββββββββββββββββββββββ β
β β task::LocalSet β β
β β β β
β β ββββββββββββ ββββββββββββ βββββββββ β β
β β β Client 1 β β Client 2 β β ... β β β
β β ββββββ¬ββββββ ββββββ¬ββββββ βββββ¬ββββ β β
β β β β β β β
β β ββββββββββββββββΌβββββββββββββ β β
β β βΌ β β
β β Rc<RefCell<Store>> β β
β β (zero-cost shared state) β β
β βββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
- Runtime:
tokio::main(flavor = "current_thread")β one OS thread, fully async. - Task spawning:
tokio::task::spawn_localβ all tasks run on the same thread. - Shared state:
Rc<RefCell<Store>>β noArc, noMutex, zero lock contention by design.
This architecture guarantees data-race freedom at compile time while delivering excellent throughput for I/O-bound workloads.
The codebase is organized into six focused modules:
src/
βββ main.rs β Entry point & runtime bootstrap
βββ server.rs β TCP listener & connection handler
βββ resp.rs β RESP protocol parser
βββ cmd.rs β Command routing & argument extraction
βββ store.rs β In-memory key-value engine
βββ response.rs β RESP response formatters
Bootstraps the single-threaded Tokio runtime and declares all modules. Calls Server::run().await and logs any fatal errors to stderr.
The Server struct binds to 127.0.0.1:7379, creates a shared store (Rc<RefCell<Store>>), and accepts incoming TCP connections inside a LocalSet. Each connection is spawned via spawn_local and runs a loop that:
- Reads raw bytes from the TCP stream into a
BytesMutbuffer - Decodes bytes into
RespValueviaResp::decode() - Constructs a
RadishCommandfrom the parsed value - Evaluates the command via
Response::eval()against the shared store - Writes the RESP-encoded response bytes back to the client
Contains the RespValue enum and the Resp struct that provides both encoding and decoding:
RespValue:SimpleString,BulkString,Integer,Array,Error,NullResp::decode(): Recursive parser that handles all RESP type prefixes (+,$,*,:,-) and returns the decoded value plus remaining unconsumed bufferResp::encode(): Serializes aRespValueback to wire format- Convenience methods:
encode_simple_string(),encode_bulk_string(),encode_error(),encode_null()
Defines CommandType (Ping, Echo, Set, Get, Ttl, Unknown) with case-insensitive matching via From<&str>. The RadishCommand struct:
- Parses raw bytes through
from_bytes()βResp::decode()βfrom_resp_value() - Extracts command name and arguments from
RespValue::Array - Exposes
cmd_type()andargs()getters
A HashMap<String, StoreValue>-backed store where each value tracks:
value: RespValueβ the stored data (native RESP values)expiry: Option<DateTime<Utc>>β optional expiration timestamp
Exposes a SharedStore type alias (Rc<RefCell<Store>>) and a Store::new() factory. Lazy expiration is implemented in get() and ttl(): if a key's expiry has passed, it is treated as non-existent. No background scanning, no timers.
The Response struct holds raw RESP-encoded bytes (Vec<u8>) and provides Response::eval() β the business logic layer that:
- Pattern-matches on
CommandTypeto dispatch each command - Handles
SETwith bothEX(seconds) andPX(milliseconds) options - Borrows the store immutably for reads (
get,ttl) and mutably for writes (set) - Returns formatted RESP responses via the
Respencoding utilities
Here's how a SET mykey hello EX 60 command flows through Radish:
redis-cli Radish Server
β β
βββββ *5\r\n$3\r\nSET\r\n... βββββββΆβ
β β
β βββββββββββ΄βββββββββββ
β β resp.rs β
β β Resp::decode() β
β β β RespValue::Array β
β βββββββββββ¬βββββββββββ
β β
β βββββββββββ΄βββββββββββ
β β cmd.rs β
β β RadishCommand:: β
β β from_resp_value() β
β β β Set + args β
β βββββββββββ¬βββββββββββ
β β
β βββββββββββ΄βββββββββββ
β β store.rs β
β β Insert into β
β β HashMap with β
β β expiry timestamp β
β βββββββββββ¬βββββββββββ
β β
β βββββββββββ΄βββββββββββ
β β response.rs β
β β Response::eval() β
β β β +OK\r\n β
β βββββββββββ¬βββββββββββ
β β
βββββββββββββ +OK\r\n ββββββββββββββββ
| Component | Technology |
|---|---|
| Language | Rust (Edition 2024) |
| Async Runtime | Tokio (single-threaded flavor) |
| Buffer Management | bytes (BytesMut) |
| Time & Expiry | chrono (DateTime<Utc>) |
| Protocol | RESP (REdis Serialization Protocol) |
- Rust (edition 2024)
redis-cli(optional, for interactive testing)
# Clone the repository
git clone https://github.com/Ansh934/radish.git
cd radish
# Boot the server on 127.0.0.1:7379
cargo run# In another terminal
redis-cli -p 7379127.0.0.1:7379> PING
PONG
127.0.0.1:7379> SET greeting "Hello, Radish!" EX 120
OK
127.0.0.1:7379> GET greeting
"Hello, Radish!"
127.0.0.1:7379> TTL greeting
(integer) 118
127.0.0.1:7379> ECHO "Radish is alive!"
"Radish is alive!"
This project is released under the Unlicense and dedicated to the public domain. See the LICENSE file for details.