A high-performance, Redis-compatible key-value datastore built from scratch in Go.
Designed with a sharded actor model to achieve parallelism across CPU cores with lock-free execution within shards. Operates via the Redis RESP protocol β fully compatible with redis-cli and standard Redis clients.
Zero external dependencies β built entirely on Go's standard library
Tested against Redis 8.0.3 on the same machine with 50 concurrent clients and 100,000 requests:
| Command | VeloxDB (req/sec) | Redis 8.0.3 (req/sec) | VeloxDB vs Redis |
|---|---|---|---|
SET |
161,030 | 177,619 | 90.7% |
GET |
151,285 | 175,131 | 86.4% |
| Command | VeloxDB (req/sec) | Redis 8.0.3 (req/sec) | VeloxDB vs Redis |
|---|---|---|---|
SET |
952,381 | 1,538,461 | 61.9% |
GET |
1,136,363 | 1,923,076 | 59.1% |
Pipelining delivers a 7x throughput boost on VeloxDB. Without pipelining, VeloxDB runs at ~87% of Redis throughput β remarkable for a pure Go implementation with no C extensions.
- Redis-compatible TCP server (RESP protocol)
- Supported commands:
GET,SET,DEL,EXPIRE,TTL,INFO - Sharded architecture β keys distributed across N shards via FNV hash
- Lock-free execution β each shard runs a single-threaded event loop
- Goroutine-per-connection for concurrent client handling
- Pipeline-aware buffered writer β flushes once per batch, not per command
- AOF persistence β crash recovery via write-ahead log replay
- Three AOF sync modes β
always,everysec,no(configurable) - RDB snapshotting β periodic JSON dump for fast restarts
- Active TTL expiry β background scanner deletes expired keys every second
- Lazy expiry β keys also checked and deleted on access
- Atomic metrics β tracks commands/sec, per-command counts, uptime via
INFO - Environment-based configuration with sensible defaults
Client (redis-cli)
β
TCP Server (accept loop)
β
Connection Handler (goroutine per client)
β
RESP Parser
β
Shard Router β fnv32a(key) % N
β
Shard Channel (buffered)
β
Shard Event Loop (single goroutine)
β
In-Memory Store (map[string]Item + TTL) + Atomic Metrics
β
AOF Persistence (appendonly.aof) + RDB Snapshot (dump.json)
β
Response β Client
RESP is a plain-text formatting protocol β an agreed-upon way to structure data so both sides know where one value ends and the next begins.
When you type SET foo bar, the client sends:
*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
*3β array of 3 elements$3β next string is 3 bytes longSET,foo,barβ the actual values\r\nβ delimiter between parts
Keys are distributed across shards using FNV hashing:
shard = fnv32a(key) % numShards
Each shard owns its data exclusively, runs a single-threaded event loop, and receives requests via a buffered channel. Multiple goroutines send to the same shard simultaneously β the channel queues them. Only one goroutine ever touches the store.
Goroutine β Request{Command, ResponseChan} β Channel β Event Loop β Store
β
ResponseChan β result
The connection handler uses a pipeline-aware buffered writer. Responses are buffered and flushed only when no more commands are waiting in the read buffer:
writer.Write(resp)
if reader.Buffered() == 0 {
writer.Flush() // flush entire batch at once
}This reduces syscall overhead under high concurrency β delivering a 7x throughput improvement with pipelined clients.
AOF (Append Only File) β every write command appended to data/appendonly.aof in RESP format after execution. Channel-based writer β one goroutine owns the buffer exclusively, eliminating race conditions without locks. Three sync modes:
| Mode | Behavior | Data Loss Risk |
|---|---|---|
always |
fsync after every write | Zero |
everysec |
fsync every second (default) | Up to 1 second |
no |
OS decides when to flush | Up to 30 seconds |
RDB (Snapshot) β periodic full JSON dump of the store to data/dump.json. Written atomically via temp file + rename β no corrupt files on crash. On startup: load RDB first, then replay only recent AOF entries. Configurable interval via RDB_INTERVAL.
Two-layer expiry for memory efficiency:
- Lazy expiry β keys checked on every
GET/TTLaccess, deleted if expired - Active expiry β background scanner per shard runs every second, deletes expired keys proactively via the shard channel
| Layer | Strategy |
|---|---|
| Networking | Goroutine per connection |
| Execution | Single goroutine per shard |
| Coordination | Buffered channels |
| AOF writes | Channel-based actor β one goroutine owns buffer |
| Persistence flush | Configurable β always/everysec/no |
| Metrics | Lock-free atomic counters |
- Go 1.22+
git clone https://github.com/AritraC1/veloxdb.git
cd veloxdb
go run cmd/server/main.goredis-cli -p 6379127.0.0.1:6379> SET foo bar
OK
127.0.0.1:6379> GET foo
"bar"
127.0.0.1:6379> EXPIRE foo 10
(integer) 1
127.0.0.1:6379> TTL foo
(integer) 8
127.0.0.1:6379> DEL foo
(integer) 1
127.0.0.1:6379> INFO
uptime_in_seconds:42
total_commands:5
commands_per_second:0.12
get_commands:1
set_commands:1
del_commands:1
expire_commands:1
ttl_commands:1
# Start real Redis on port 6380
redis-server --port 6380
# Run benchmark (standard)
./scripts/benchmark.sh
# Run benchmark (pipelined)
redis-benchmark -p 6379 -n 100000 -c 50 -q -t set,get -P 16All config is read from environment variables with sensible defaults:
| Variable | Default | Description |
|---|---|---|
PORT |
6379 |
TCP port |
NUM_SHARDS |
8 |
Number of shards |
BUFFER_SIZE |
64 |
Shard channel buffer |
AOF_PATH |
data/appendonly.aof |
AOF file path |
AOF_SYNC_MODE |
everysec |
AOF sync strategy β always, everysec, no |
RDB_PATH |
data/dump.json |
RDB snapshot path |
RDB_INTERVAL |
60 |
Snapshot interval in seconds |
PORT=6380 NUM_SHARDS=16 AOF_SYNC_MODE=always go run cmd/server/main.goveloxdb/
βββ cmd/server/main.go # Entry point
βββ internal/
β βββ server/ # TCP server, connection handler, buffered writer
β βββ protocol/ # RESP parser + encoder
β βββ shard/ # Shard, event loop, manager, TTL scanner
β βββ store/ # In-memory KV store, Item, TTL expiry
β βββ command/ # Stateless command handlers + registry
β βββ persistence/ # AOF writer, RDB snapshot, replay
β βββ metrics/ # Atomic metrics + INFO command
β βββ config/ # Environment-based config
βββ scripts/
β βββ benchmark.sh # Benchmark vs Redis
βββ data/ # AOF + RDB files (gitignored)
Why sharding over a global mutex? A single sync.RWMutex becomes a bottleneck under concurrent writes. Sharding distributes keys across independent stores β each with its own event loop β so writes to different keys execute in parallel with no coordination overhead.
Why channels over locks within shards? Each shard's event loop is the only goroutine that touches its store. Channels provide the serialization β no sync.Mutex needed inside shards. This is the actor model applied to data storage.
Why a channel-based AOF writer? AOF is shared across all shards. Instead of a mutex, the AOF has its own goroutine and channel β same actor model as shards. One goroutine owns the buffer exclusively, eliminating race conditions without locking. Natural batching falls out of this design.
Why three AOF sync modes? Different use cases have different durability requirements. always for zero data loss, everysec for the best balance (Redis's default), no for maximum throughput when durability is handled by replication.
Why RDB + AOF together? AOF alone means replaying potentially millions of entries on restart. RDB gives a fast baseline β load snapshot, replay only recent AOF. Same approach Redis uses.
Why active + lazy TTL expiry? Lazy expiry alone means expired keys stay in memory until accessed β wasteful for short-lived keys nobody reads. Active expiry proactively cleans up every second via the shard channel β no locks, consistent with the actor model.
Why FNV hash? Fast, deterministic, good key distribution. No cryptographic overhead needed for routing.
Why atomic counters for metrics? Metrics are updated on every command across all goroutines simultaneously. Atomic operations (~1-2ns) cost 10-15x less than a mutex (~20-30ns) and never block β consistent with the lock-free design philosophy.
Why pipeline-aware flushing? Calling conn.Write per command makes one syscall per response. Buffering responses and flushing when the read buffer is empty batches multiple responses into one syscall β reducing kernel overhead under pipelined workloads.
- Not fully Redis-compatible β only
GET,SET,DEL,EXPIRE,TTL,INFO - No transactions (
MULTI/EXEC) - No pub/sub
- Hot-key bottleneck β if all clients hit the same key, one shard becomes a bottleneck
- Fixed shard count β resharding requires a restart
- Cross-key atomicity not supported across shards
- RDB uses JSON β binary format would be 3x smaller and 4x faster to load
- TCP server + RESP protocol
- GET, SET, DEL, EXPIRE, TTL
- Sharded actor model
- AOF persistence + crash recovery
- RESP encoder β centralized response formatting
- Environment-based config
- Pipeline-aware buffered writer β 7x throughput under pipelining
- Atomic metrics + INFO command
- Benchmarking vs Redis
- Active TTL expiry β background scanner per shard
- Channel-based AOF writer β lock-free, three sync modes
- RDB snapshotting β atomic JSON dump, configurable interval
- Consistent hashing β reshard without restart
- Replication β primary-replica model
- Cluster mode
- More commands β INCR, DECR, EXISTS, MGET, MSET
- LRU/LFU eviction
- Transactions (MULTI/EXEC)
- Pub/Sub
- Custom TCP server and binary protocol implementation (RESP)
- Concurrent system design in Go β goroutines, channels, no mutexes
- Sharded actor model for parallel, lock-free execution
- Pipeline-aware I/O for high-throughput batch processing
- Dual persistence β AOF for durability, RDB for fast restarts
- Two-layer TTL expiry β lazy + active
- Channel-based AOF writer β lock-free shared resource without mutex
- Lock-free metrics with atomic counters
- Tradeoffs in distributed systems design β sharding, consistency, durability
Apache License 2.0. See LICENSE for details.
Inspired by the design of Redis and modern in-memory data systems.