Redis keeps everything in RAM. VeltrixDB doesn't have to.
Redis's speed comes from one thing: all data lives in memory. That's a real advantage — and a real cost ceiling. VeltrixDB keeps only the index and a LIRS cache in DRAM and stores values on NVMe, so hot keys are answered from memory and cold data is priced like disk, not like RAM.
This is not a "Redis killer" comparison. Redis is the right choice for millions of workloads. But there is a specific class of problem — large keyspaces under sustained write pressure — where keeping every byte in RAM, and forking to persist it, gets expensive and noisy.
When Redis is under heavy write load, the fork behind an AOF rewrite or RDB save can make P99 spike. Engineers work around this with replica reads, fork tuning and alert rules. VeltrixDB does not fork to persist: every write goes to a group-commit WAL and is fdatasync'd before it is acknowledged.
The P50 is similar. The P99 shape is not.
Both databases are fast. The difference is in the tail — and in what each design has to do in the background to stay durable. Every VeltrixDB cell below says how it was measured.
| Metric | Redis (AOF on) | VeltrixDB | Source & conditions |
|---|---|---|---|
| Read over network · avg / P99 | ~100–300 µs / 1–3 ms | 461 µs / 2,771 µs | VeltrixDB: measured, YCSB 0.17, 200 threads, 100M keys, all cache hits (pure-Go build). Redis: production reports |
| Read P99 under sustained writes | 1–50 ms (spikes) | ~510 µs | VeltrixDB: internal, unverified 3-node GKE run, not reproduced with a published harness. Redis: AOF rewrite / fork pauses, production reports |
| Durable single write · P99 | varies with AOF | 47.3 ms | VeltrixDB: measured, YCSB load, WAL + fdatasync before every ACK, 5 ms fixed flush windows (before adaptive group commit, v1.12), 18,064 ops/s |
| Batch writes (1024-key MPUT) | pipelined, single-threaded | 3.54M keys/s · P99 4.2 ms | VeltrixDB: measured, 8 clients, 1M-key space, macOS, v1.7. 1.77M keys/s on a 4-CPU CI runner (tmpfs, same-host client) |
| In-process cache lookup | — | ~92 ns | VeltrixDB: the sharded LIRS lookup alone, no network. Not comparable with a network round trip |
Redis numbers above reflect production reports from teams running AOF with appendfsync everysec under mixed read/write load; they are not from a side-by-side run on the same hardware. Your numbers depend on instance size, fork latency, and background save frequency.
The GC death spiral — and why VeltrixDB can't get stuck in it. VeltrixDB's own value-log GC backs off when read latency rises, to protect reads. Done naively, that is a spiral: GC pauses, garbage accumulates, reads stay slow, GC stays paused. The GC escalates in three tiers, and at 65% garbage it bypasses the read-pressure pause unconditionally, so it cannot stay paused forever (a stale pause also clears after 4 minutes without reads). Redis has no value-log GC — its tail risk is the fork. veltrixdb_vlog_gc_emergency_runs_total counts every time that happens.
VeltrixDB's RAM scales with keys, not value bytes.
VeltrixDB moves values to NVMe, but the whole index stays in RAM. So the RAM saving depends on value size: with small values it is small; with 1 KB values it is roughly 4–7×. The table below is per-key arithmetic, not a measurement.
| Keys · value | Redis RAM (estimate) | VeltrixDB RAM (index) | VeltrixDB NVMe (packed values) |
|---|---|---|---|
| 100M · 128 B | ~25 GB | ~14–23 GB | ~15 GB |
| 1B · 128 B | ~250 GB | ~142–232 GB | ~160 GB |
| 100M · 1 KB | > 100 GB | ~14–23 GB | ~137 GB |
| 1B · 1 KB | > 1 TB | ~142–232 GB | ~1.4 TB |
The VeltrixDB RAM column is the index: ~142 B per key with the native index (measured at 5M keys), plus ~90 B per key for the ordered index that serves RANGE / SCANCUR unless you start with --disable-ordered-index — plus whatever LIRS cache you configure. The Redis column is an estimate of values plus per-key overhead; we have not measured it. Nothing at 1 billion keys has been measured reproducibly.
With 128-byte values the index is about as large as the values, so VeltrixDB saves little RAM there; its advantage at that size is NVMe durability without fork-based persistence. The disk side relies on block packing: batched writes (MultiPut, and GC relocation) put up to ~26 records into one 4 KB VLog block, ~152 B per 128-byte value. A single Put stays on the unpacked path and takes a whole 4 KB block. Price RAM and NVMe for your own instance types rather than relying on a fixed ratio.
If your dataset fits comfortably in RAM and you're not cost-constrained — Redis is the simpler choice. If your keyspace is growing, your values are large, and the RAM bill is growing with them, that's the signal to benchmark VeltrixDB against your workload.
Every value should land on NVMe once — until GC moves it.
Write amplification is how many times a byte gets written to storage for every one byte of user data. At 1× you're paying for your data once. At 30× you're paying thirty times — in SSD wear, in NVMe IOPS, and in latency tail.
| Redis (AOF rewrite) | 2–5× — AOF rewrite rewrites the full dataset to a new file |
| RocksDB / LevelDB | 10–30× — LSM compaction sorts and rewrites every key multiple times across levels |
| VeltrixDB | ~1× by design for batched writes — values are appended once to the Value Log and never sorted; the WAL carries only a header and the key. GC copies still-live values forward when it reclaims a region, and a single small Put pads to a whole 4 KB block, so the real figure is higher. Not measured. |
VeltrixDB uses WiscKey key-value separation: the per-shard in-memory index (8192 shards) holds a 64-byte entry per key — offset, disk index, on-disk and plaintext sizes, CRC32C, write time, TTL, flags. Values go to an append-only Value Log on NVMe. There is no LSM compaction; the value-log GC relocates live values out of mostly-dead regions and skips keys written in the last 30 s. With a low overwrite rate, write amplification stays close to 1×; heavy overwrites make GC do more copying.
What this means for SSDs (arithmetic, not a benchmark). At 100K batched writes/sec of 128 B values, 1× write amp writes 12.8 MB/s to NVMe (unbatched single Puts would pad each to 4 KB). At 10× (a typical LSM figure) that's 128 MB/s — proportionally more SSD wear and more IOPS to provision.
Redis is the right choice in these situations.
This is not a takedown of Redis. It is the right tool for a large number of workloads. Knowing when not to migrate is as important as knowing when to.
- Your full dataset fits comfortably in RAM — if you have 5M keys at 256 bytes, Redis's DRAM-only path has zero NVMe latency on any read. The advantage of VeltrixDB's cache tier disappears.
- You need pub/sub, streams, sorted sets, or Lua scripting — Redis has a rich data structure model built over decades. VeltrixDB is a key-value store. VeltrixDB has hashes (with per-field TTL), lists, sets, counters, CAS, TTLs, ordered key range scans, secondary indexes and one-shot transactions — but none of those four. If your application relies on
ZRANGEBYSCORE,XREAD,PUBLISHor Lua, stay on Redis. - Your team already runs Redis and has no latency complaints — operational familiarity is real value. Don't fix what isn't broken.
- You need a fully-managed hosted service today — Redis Cloud, ElastiCache, Upstash, and Momento all exist and are production-hardened. VeltrixDB is self-hosted only today; a managed service is planned, not available.
- Your workload is write-once, read-many and fits in RAM — with no overwrites there is little background rewriting on either side, so VeltrixDB's tail-latency design has little to fix.
The specific problems VeltrixDB was built to solve.
VeltrixDB earns its place when your dataset is outgrowing your RAM budget, when your production dashboards show Redis P99 spikes you can't explain, or when you're running on Kubernetes with local NVMe.
- Your dataset doesn't fit in RAM — or fitting it costs too much — VeltrixDB's LIRS cache keeps hot keys in DRAM (default
-cache256 MB; size it to your hot set) and a miss costs onepreadof a 4 KB VLog block. Your hit rate depends on your access skew — measure it withveltrixdb_cache_hit_rate. - Unpredictable P99 under sustained write pressure — if your Redis P99 spikes during AOF rewrite or background save: VeltrixDB never forks, and its admission-controlled three-tier GC is designed to keep background work out of the read tail. That is a design goal; benchmark it on your workload rather than taking a ceiling on faith.
- High-concurrency bulk writes — MultiPut with 1024-key batches measured 3.54M keys/s (8 clients, 1M-key space, macOS, v1.7) and 1.77M keys/s on a 4-CPU CI runner (tmpfs, same-host client). VLog appends reserve offsets with an atomic add, and a batch is one
pwriteplus onefdatasync. - Kubernetes with local NVMe SSDs — a Kubernetes Operator and a Helm chart (with an nvme-prep DaemonSet that formats local SSDs as XFS) live in separate repositories; the main repository ships the
kubectl-veltrixplugin and Prometheus rules. - Write amplification and SSD cost matter — values are written once and never re-sorted, so there is no LSM-style 10–30× rewrite; GC relocation and 4 KB padding of single
Puts are the extra bytes. - You need predictable latency at scale — not just at P50 — an internal 3-node GKE run (unverified, not reproduced with a published harness) recorded zero GC emergency runs over 30 minutes of sustained 80R/20W load. The reproducible numbers are on the performance page.
Everything side by side, nothing hidden.
An honest matrix. We've marked items we're still building as roadmap rather than pretending they exist.
| Feature | Redis | VeltrixDB |
|---|---|---|
| In-memory lookup (< 1 µs, in-process) | ✓ | ✓ ~92 ns cache hit |
| No fork for persistence | ⚠ AOF rewrite / RDB fork | ✓ group-commit WAL |
| NVMe storage tier | — | ✓ |
| Values never re-sorted (WiscKey) | — | ✓ |
| Value-log GC that cannot be permanently paused | n/a · no value-log GC | ✓ three-tier |
| Hashes / lists / sets | ✓ | ✓ HSET (per-field TTL), LPUSH, SADD … |
| Pub/Sub | ✓ | — |
| Sorted sets / ZRANGEBYSCORE | ✓ | — |
| Ordered range scans | ✓ sorted sets / SCAN | ✓ RANGE / SCANCUR (local node in clusters) |
| Streams (XREAD/XADD) | ✓ | — |
| Lua scripting | ✓ | — |
| Atomic CAS / INCR / DECR / SETNX | ✓ | ✓ |
| Multi-key transactions | ✓ MULTI / EXEC, WATCH | ✓ one-shot TXN, version check, read-committed |
| Secondary indexes | ⚠ Redis Query Engine / RediSearch | ✓ IDXCREATE / IDXQUERY (equality) |
| Vector · full-text · hybrid search | ⚠ Redis Query Engine / RediSearch | ✓ built in — see §08 |
| TTL / key expiry | ✓ | ✓ PUTEX / binary MPUT per-entry TTL |
| Encryption at rest (AES-256-GCM) | ✓ | ✓ |
| TLS in transit | ✓ | ✓ |
| Multi-node replication | ✓ | ✓ Raft, or replicated (eventual default / quorum / strong) |
| Kubernetes Operator | ⚠ third-party | ✓ separate repository |
| Prometheus metrics | ✓ | ✓ |
| Change data capture (CDC) | — | ✓ built-in |
| Go / Python / Node / Rust / C++ SDKs | ✓ | ✓ Go in the main repo; others in Veltrixdb-client |
| RESP protocol compatibility | ✓ | roadmap · not implemented |
| Managed cloud service | ✓ | planned · self-hosted today |
Against Redis Stack-style search, feature by feature.
Redis does vector, full-text and hybrid queries through the Redis Query Engine (the RediSearch module, shipped in Redis Stack and bundled with Redis 8). VeltrixDB has its own built in since v1.11. This is a feature comparison only: no performance comparison has been run on shared hardware, so none is shown.
| Capability | Redis Query Engine | VeltrixDB |
|---|---|---|
| Vector index | ✓ HNSW and flat | ✓ HNSW; k = 0 is an exact scan |
| Distance | ✓ cosine / L2 / inner product | cosine only |
| Where vectors live | RAM, with the data | float32 on NVMe; RAM holds float32, int8 or PQ codes + graph |
| Compressed vectors | depends on version — check its docs | ✓ int8, PQ, layer-0 graph in a mapped file; re-rank against float32 |
| Full-text | ✓ stemming, phrases, field weights | BM25, Unicode tokenizer; no stemming, phrases or field weights |
| Hybrid vector + text | ✓ KNN with a filter query | ✓ weighted reciprocal-rank fusion (HSEARCH) |
| Metadata filters | ✓ tag / numeric / text fields | ✓ = != < > contains on the record with the same id; = served by a secondary index |
| Index after restart | see Redis docs for your persistence setup | rebuilt from durable keys; searches refused until done |
| Clustered search | depends on deployment | fans out to every node, fails closed if a peer misses its timeout |
| Client access | ✓ every Redis client | Go client, or text / binary protocol |
VeltrixDB’s own numbers, each from its docs with conditions: 768-dim heap per vector 3,383 B float32, 1,078 B int8, 493 B PQ (10K synthetic, macOS); GloVe-100 (100K real vectors) recall@10 0.953 at ef = 256, p50 0.77 ms, one query at a time on macOS. It has been tested to 100K real vectors; nothing at 10M+. Commands, layouts and limits: vector & hybrid search docs.
A client change, not a connection-string swap.
VeltrixDB does not speak RESP, so redis-cli and Redis client libraries cannot connect to it. Every call site moves to a VeltrixDB client or to its own text / binary protocol. Many Redis commands have a close equivalent; some have none.
Try it in 30 seconds
# Linux / Windows (the image is linux/amd64) docker run -p 9000:9000 ghcr.io/veltrixdb/veltrixdb:latest # Mac (Apple Silicon) — runs under emulation docker run --platform linux/amd64 -p 9000:9000 ghcr.io/veltrixdb/veltrixdb:latest # Text protocol over nc — SET is accepted as an alias of PUT echo -e "SET hello world\nGET hello\nPING" | nc localhost 9000
Command mapping
| SET / GET / DEL | PUT (alias SET) / GET / DEL — text-protocol PUT takes the rest of the line as the value |
| SET … EX / SETEX | PUTEX key ttl-seconds value, or the per-entry TTL of a binary MPUT (the binary PUT frame has no TTL field). There is no standalone EXPIRE / TTL on plain keys |
| SETNX / INCR / DECR | SETNX, INCR key [delta], DECR key [delta]; compare-and-swap is CAS key expected new |
| MGET / pipelines | MGET; binary MPUT / MGET batches (one WAL write, one VLog pwrite, one fdatasync per disk) |
| Hashes | HSET key field value [ttl], HGET, HDEL, HGETALL, HKEYS, HLEN, HEXPIRE, HTTL |
| Lists / sets | LPUSH RPUSH LPOP RPOP LRANGE LLEN; SADD SREM SMEMBERS SISMEMBER SCARD |
| SCAN / KEYS | SCANCUR cursor limit, RANGE start end [LIMIT n] [REV] — ordered, local node only in a cluster |
| MULTI / EXEC / WATCH | TXN n + n lines of SET / SETIF key version value / DEL: one-shot, optimistic, read-committed, not MVCC; a version conflict returns CONFLICT — retry |
| ZADD, PUBLISH, XADD, EVAL | No equivalent. Keep those workloads on Redis |
Copy the data
There is no Redis importer. veltrix-migrate import (in the main repo) reads JSONL from stdin, one {"k": key, "v": base64 value} per line, and PUTs each entry. For string keys, a short redis-py script produces that format. It does not carry TTLs, and other Redis types need their own mapping (e.g. hash fields to HSET):
cat > dump_strings.py <<'EOF' import base64, json, sys, redis r = redis.Redis(host='localhost', port=6379) for k in r.scan_iter(count=1000): if r.type(k) != b'string': continue v = r.get(k) if v is not None: sys.stdout.write(json.dumps({"k": k.decode(), "v": base64.b64encode(v).decode()}) + "\n") EOF go build -o veltrix-migrate ./cmd/veltrix-migrate # in the VeltrixDB repo python3 dump_strings.py | ./veltrix-migrate import --dst localhost:9000
veltrix-migrate sends one PUT at a time, so each value takes a whole 4 KB VLog block; for many small values, load with batched MPUTs instead (the Go client’s BinaryConn.MPut) to get block packing. In --mode=raft, point --dst at the leader.
SDK migration
# Before (redis-py) import redis r = redis.Redis(host='localhost', port=6379) r.set('key', 'value') val = r.get('key') # After (veltrixdb-python) import veltrixdb r = veltrixdb.Client(host='localhost', port=9000) r.put('key', 'value') val = r.get('key')
// Before (ioredis) const Redis = require('ioredis') const r = new Redis({ host: 'localhost', port: 6379 }) await r.set('key', 'value') // After (veltrixdb-client) const { VeltrixDBClient } = require('veltrixdb-client') const c = new VeltrixDBClient({ host: 'localhost', port: 9000 }) await c.connect() await c.put('key', Buffer.from('value'))
The search API (vectors, text, hybrid) is only in the Go client in the main repo; from other languages use the text-protocol search commands over a TCP socket. RESP compatibility is on the roadmap, with no date.
Recommended migration strategy. Copy the data, then run VeltrixDB in shadow mode — dual-write to both Redis and VeltrixDB for two weeks, compare read results, validate latency against your actual key distribution. Cut over when the shadow reads match. It gives you real-world latency data before you commit; the migration playbook walks through the stages.