Vole is Haitian Creole for to fly. We built this project so your data can do exactly that.
Vole is an in-memory data store that compiles to a single binary with no external dependencies. It speaks the RESP wire protocol, which means every existing client library already works with it -- Go, Python, Node, Java, Ruby, you name it. Just point your client at Vole and go.
But Vole isn't just another key-value store. We got tired of stitching together five different systems just to handle caching, queues, rate limiting, pub/sub, and time-series data. So we put all of it in one place -- with native multi-node multi-master replication built in, so every node in your cluster can accept writes without a single point of failure.
"I just want to query by value." Key-value stores make you look things up by key. That's it. Need to find all users older than 18? You're on your own. Vole's HSEARCH lets you query hash fields directly: HSEARCH user:* WHERE age > 18 AND status = active.
"My queue eats messages." Pop-based queues throw messages away the moment a consumer picks them up. Consumer crashes? Message gone. Vole tracks every message through processing. If nobody acknowledges it, it goes back in the queue. After too many failures, it lands in a dead-letter queue where you can inspect it.
"I need a rate limiter and I don't want to write Lua." One command: RATELIMIT api:user:123 100 60. Done. Sliding window, returns remaining quota, retry-after, the works.
"I need an HTTP endpoint for this." Start Vole with --http-addr :8080 and every operation is available as a JSON API. No client library required. curl works fine.
"Keys just vanish and nobody knows." When a key expires in most stores, it's just gone. Vole can fire a webhook or publish a pub/sub event when that happens, so your app can actually react to it.
"If the primary goes down, writes stop." Most data stores give you leader-follower replication -- one node handles writes, the rest are read-only copies. If the leader dies, you're scrambling to promote a follower. Vole has native multi-master replication. Every node accepts writes. Changes propagate to all peers automatically. Lose a node? The others keep going. No election, no downtime, no manual promotion. Start two nodes with --multimaster and they stay in sync.
"I have staging and production data in the same instance." Named namespaces. NAMESPACE CREATE staging, NAMESPACE USE staging. Keys are completely isolated. No more prefixing everything with env:.
- Getting started -- your first 10 minutes with Vole
- Installation -- building from source, Docker, systemd, upgrading
- Configuration -- every flag explained, with examples and a production template
go build -o vole ./cmd/vole
go build -o vole-cli ./cmd/vole-cli
# Simplest possible start
vole
# With the HTTP API
vole --http-addr :8080
# With persistence that survives restarts
vole --appendonly --snapshot vole.snap --snapshot-interval 5m
# With TLS and a password
vole --tls-cert cert.pem --tls-key key.pem --requirepass mysecret
# With memory limits
vole --maxmemory 536870912 --maxmemory-policy allkeys-lru
# Multi-master replication (both nodes accept writes)
vole --addr :7379 --node-id node1 --peers "node2@localhost:7380" --multimaster
vole --addr :7380 --node-id node2 --peers "node1@localhost:7379" --multimaster
# Or use a config file
cp vole.conf.example /etc/vole/vole.conf
vole --config /etc/vole/vole.conf# Single commands
vole-cli SET hello world
vole-cli GET hello
# Connect to a specific host
vole-cli -h 10.0.0.5 -p 7379 PING
# Interactive shell
vole-cli
127.0.0.1:7379> SET user:1 "John"
OK
127.0.0.1:7379> GET user:1
"John"
127.0.0.1:7379> helpType help in the interactive shell to see every available command grouped by category. Every command Vole supports -- including replication, multi-master, namespaces, scripting, queues, and all the rest -- works directly from the CLI. A few examples:
# Replication
vole-cli REPLICAOF localhost 7380
vole-cli REPLICAOF NO ONE
vole-cli MULTIMASTER ENABLE
vole-cli MULTIMASTER STATUS
# Namespaces
vole-cli NAMESPACE CREATE staging
vole-cli NAMESPACE USE staging
# Queues
vole-cli ENQUEUE tasks '{"type":"email"}'
vole-cli DEQUEUE tasks TIMEOUT 10
# JSON
vole-cli JSON.SET user:1 '$' '{"name":"Alice","age":30}'
vole-cli JSON.GET user:1 '$.name'
# Rate limiting
vole-cli RATELIMIT api:user:42 100 60
# Time-series
vole-cli TS.ADD metrics:cpu '*' 72.5
# Cluster management
vole-cli CLUSTER MEET host2:7380
vole-cli CLUSTER NODES| Type | What it is |
|---|---|
| String | The basics. Binary-safe, optional TTL. |
| Hash | Field-value maps. Think of a row in a table. |
| List | Double-ended queue backed by a ring buffer. Push and pop from either end in O(1). |
| Set | Unique unordered strings. |
| Sorted Set | Unique strings ordered by score. The sorted order is maintained on every write, so range queries are fast. |
| Stream | Append-only log with consumer groups, acknowledgment, and blocking reads. |
| Bitmap | Bit-level operations on string values. |
| HyperLogLog | Probabilistic cardinality counting. Useful for "how many unique visitors" without storing every visitor. |
| JSON | Structured documents you can read and write with dot-path syntax like $.address.city. |
| Time-Series | Timestamped numeric samples with labels and downsampling (avg, min, max, sum, count). |
Start with --http-addr :8080. Everything comes back as JSON.
# Strings
curl -X PUT localhost:8080/api/v1/keys/mykey -d '{"value":"hello","ex":3600}'
curl localhost:8080/api/v1/keys/mykey
curl -X DELETE localhost:8080/api/v1/keys/mykey
# Hashes
curl -X PUT localhost:8080/api/v1/hash/user:1 -d '{"name":"John","age":"30"}'
curl localhost:8080/api/v1/hash/user:1
# Lists
curl -X POST localhost:8080/api/v1/list/tasks/push -d '{"values":["a","b"],"side":"left"}'
curl "localhost:8080/api/v1/list/tasks?start=0&stop=-1"
# Sets
curl -X POST localhost:8080/api/v1/set/tags -d '{"members":["go","database"]}'
curl localhost:8080/api/v1/set/tags
# Sorted sets
curl -X POST localhost:8080/api/v1/zset/scores \
-d '{"members":[{"member":"alice","score":100}]}'
# Pub/sub
curl -X POST localhost:8080/api/v1/publish/events -d '{"message":"hello"}'
# Rate limiting -- returns 200 or 429 with standard rate limit headers
curl -X POST localhost:8080/api/v1/ratelimit/api:user:123 -d '{"max":100,"window":60}'
# Search hashes by field value
curl "localhost:8080/api/v1/search/hash?pattern=user:*&where=age>18&limit=50"
# Stream key changes in real time (Server-Sent Events)
curl -N localhost:8080/api/v1/events?patterns=__keyspace__:user:*
# Webhooks
curl -X POST localhost:8080/api/v1/webhooks \
-d '{"pattern":"session:*","event":"expired","url":"https://example.com/hook"}'
# Prometheus metrics
curl localhost:8080/metrics
# Health check
curl localhost:8080/healthRATELIMIT api:user:123 100 60
One command. 100 requests per 60 seconds, sliding window. Returns four values: allowed (1/0), remaining requests, retry-after in milliseconds, and when the window resets.
RATELIMIT.PEEK api:user:123 -- check without consuming a request
RATELIMIT.RESET api:user:123 -- clear the counter
JSON.SET user:1 $ '{"name":"John","age":30,"address":{"city":"NYC"}}'
JSON.GET user:1 $.address.city -- "NYC"
JSON.NUMINCRBY user:1 $.age 1 -- 31
JSON.DEL user:1 $.address
JSON.TYPE user:1 $.name -- "string"
JSON.KEYS user:1 $ -- ["name", "age"]
JSON.SET items $ '["a","b"]'
JSON.ARRAPPEND items $ '"c"' -- 3
JSON.ARRLEN items $ -- 3
ENQUEUE tasks '{"type":"email","to":"user@example.com"}'
ENQUEUE tasks '{"type":"cleanup"}' DELAY 300
DEQUEUE tasks TIMEOUT 30
-- returns [message-id, body, retry-count]
QACK tasks <message-id> -- done processing
QNACK tasks <message-id> -- put it back, try again
QLEN tasks -- how many are waiting
QINFO tasks -- pending / processing / dead-letter counts
QDEAD tasks COUNT 10 -- look at what failed
Messages that fail too many times end up in the dead-letter queue instead of disappearing.
TS.ADD metrics:cpu * 72.5 LABELS host=web1 region=us-east
TS.RANGE metrics:cpu - + COUNT 100
TS.GET metrics:cpu
TS.INFO metrics:cpu
TS.DOWNSAMPLE metrics:cpu metrics:cpu:hourly avg 0 9999999999999 3600000
Aggregations: avg, sum, min, max, count, first, last.
HSET user:1 name John age 25 status active
HSET user:2 name Jane age 32 status active
HSET user:3 name Bob age 17 status inactive
HSEARCH user:* WHERE age > 18 AND status = active LIMIT 0 50
Operators: =, !=, >, <, >=, <=, CONTAINS, STARTSWITH.
NAMESPACE CREATE analytics
NAMESPACE USE analytics
SET pageview:home 42 -- only exists in "analytics"
NAMESPACE USE default
GET pageview:home -- (nil)
NAMESPACE LIST
NAMESPACE DROP analytics
TAG user:1 env=prod region=us-east tier=premium
TAGQUERY env=prod AND tier=premium LIMIT 100
TAGGET user:1
TAGDEL user:1 region
SCHEMA.SET user:* name:string age:int email:email
HSET user:1 name John age 25 email john@example.com -- fine
HSET user:2 name Jane age notanumber -- rejected
Types: string, int, float, bool, email, required.
SET announcement "Big news!" AFTER 3600
SETDELAYED config:flag "enabled" 86400 EX 7200
The key is invisible until the delay passes. GET, EXISTS, KEYS all act as if it doesn't exist yet.
WEBHOOK REGISTER session:* expired https://example.com/session-expired
WEBHOOK LIST
WEBHOOK UNREGISTER session:* expired https://example.com/session-expired
When a matching key expires (or is set/deleted, depending on the event), Vole sends an HTTP POST with the key name, event type, and timestamp.
Vole publishes every key mutation to __keyspace__:<key> and __keyevent__:<event> channels. You can subscribe via RESP (SUBSCRIBE, PSUBSCRIBE) or via the HTTP SSE endpoint.
CRON.ADD cleanup "0 */6 * * *" DEL temp:*
CRON.LIST
CRON.INFO cleanup
CRON.DEL cleanup
Standard 5-field cron syntax. The command runs inside the server -- no shell, no external process.
Disabled by default. When enabled, Vole records every write with the key, command, timestamp, and client address.
AUDIT.ENABLE
SET user:1 John
AUDIT user:1 COUNT 5
AUDIT.SEARCH user:* COUNT 50
AUDIT.DISABLE
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
EVALSHA <sha1> 1 mykey
Vole's script engine handles sequences of redis.call() with KEYS[n]/ARGV[n] substitution, variable assignment, and return values. It covers the patterns you'll see in practice. It is not a full Lua VM -- things like loops, coroutines, and require won't work.
Your data survives restarts. Two mechanisms, usable together or separately.
Every write goes to a log file. Each entry has a CRC32 checksum, so corrupted entries are caught and skipped on replay.
| Fsync | What it does |
|---|---|
always |
Flush to disk after every write. Safest. Slowest. |
everysec |
Flush once a second. Good default. |
no |
Let the OS decide. Fastest. |
vole --appendonly --appendfilename vole.aof --appendfsync everysecFull point-in-time dumps. All 10 data types are included.
vole --snapshot vole.snap --snapshot-interval 5mSAVE -- snapshot now (blocks)
BGSAVE -- snapshot in background
LASTSAVE -- when was the last one
On startup: load snapshot, then replay AOF. After each snapshot, the AOF is truncated.
Vole supports two replication modes: leader-follower and multi-master.
One node is the leader, the rest are read-only followers. A follower connects, gets a full snapshot, then streams every write from the leader.
vole --addr :7379 # leader
vole --addr :7380 --replicaof localhost:7379 # followerREPLICAOF localhost 7379 -- start following at runtime
REPLICAOF NO ONE -- stop following, become standalone
Followers reject writes with READONLY. Check INFO for replication status.
Worth knowing: replication is asynchronous. There is no automatic failover -- you promote a follower manually. If a follower reconnects, it gets a fresh snapshot (no partial resync).
Every node accepts writes. Changes propagate to all peers automatically. There's no single point of failure for writes -- if one node goes down, the others keep working.
# Start two nodes that replicate to each other
vole --addr :7379 --node-id node1 --peers "node2@localhost:7380" --multimaster
vole --addr :7380 --node-id node2 --peers "node1@localhost:7379" --multimasterWrite to either node and the data shows up on the other:
# On node 1
vole-cli -p 7379 SET user:1 Alice
# On node 2 (it's already there)
vole-cli -p 7380 GET user:1
# "Alice"You can also enable it at runtime:
MULTIMASTER ENABLE
CLUSTER MEET localhost:7380
Monitor the state:
MULTIMASTER STATUS -- enabled + peer count
MULTIMASTER PEERS -- list of connected peers
When a new node joins, it receives a snapshot from an existing peer if its store is empty, then starts streaming live writes in both directions. Writes from peers are applied locally but never re-propagated, so there are no infinite loops.
Worth knowing: multi-master replication is asynchronous. If two nodes write to the same key at the same instant, the last write to arrive wins. There is no conflict merging -- it's last-writer-wins.
16,384 hash slots distributed across nodes. Commands for keys on another node get a MOVED redirect -- your client follows it.
vole --addr :7379 --node-id node1 --peers "node2@host2:7380"CLUSTER MEET host2:7380
CLUSTER FORGET <node-id>
CLUSTER NODES
CLUSTER INFO
CLUSTER KEYSLOT mykey
A background heartbeat pings peers every 5 seconds and tracks their state. Slot ownership rebalances when nodes join or leave.
Worth knowing: Vole does not forward commands on your behalf -- clients must handle MOVED redirects. There is no live data migration when slots move; only the ownership metadata changes.
# Require a password
vole --requirepass mysecret
# Encrypt everything with TLS
vole --tls-cert cert.pem --tls-key key.pemTLS covers both the RESP protocol and the HTTP API. Minimum version is TLS 1.2.
/metrics on the HTTP port. Exposes commands processed, connections, keyspace hit/miss rate, memory usage, goroutine count, uptime.
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET
Threshold is 10ms. Logs the command, duration, and client address.
CLIENT LIST
CLIENT ID
CLIENT SETNAME myapp
CLIENT GETNAME
CLIENT KILL ID 42
INFO
DBSIZE
TIME
When --maxmemory is set and usage exceeds the limit:
| Policy | What happens |
|---|---|
noeviction |
Writes fail with OOM error (default) |
allkeys-random |
Random keys get deleted |
volatile-random |
Random keys with a TTL get deleted |
allkeys-lru |
Least recently accessed keys get deleted (samples 5 keys per cycle) |
vole --maxmemory 536870912 --maxmemory-policy allkeys-lruAlso configurable at runtime via CONFIG SET.
GET SET MGET MSET MSETNX SETNX SETEX PSETEX GETSET GETEX GETDEL GETRANGE SETRANGE SUBSTR INCR INCRBY DECR DECRBY INCRBYFLOAT APPEND STRLEN
HSET HGET HGETALL HDEL HEXISTS HKEYS HVALS HLEN HINCRBY HSETNX HRANDFIELD
LPUSH RPUSH LPOP RPOP LRANGE LLEN LINDEX LSET LINSERT LPOS LREM BLPOP BRPOP RPOPLPUSH LMOVE
SADD SREM SMEMBERS SISMEMBER SCARD SRANDMEMBER SMOVE SPOP SINTER SINTERCARD SINTERSTORE SUNION SUNIONSTORE SDIFF SDIFFSTORE
ZADD ZRANGE ZRANGEBYSCORE ZRANGEBYLEX ZREVRANGE ZREM ZSCORE ZCARD ZRANK ZREVRANK ZCOUNT ZPOPMIN ZPOPMAX ZINCRBY
PFADD PFCOUNT PFMERGE
XADD XRANGE XREAD XLEN XTRIM XINFO XGROUP CREATE XREADGROUP XACK XCLAIM XAUTOCLAIM XPENDING
GEOADD GEOPOS GEODIST GEOSEARCH
SETBIT GETBIT BITCOUNT BITOP BITPOS
DEL UNLINK EXISTS TYPE KEYS SCAN RENAME COPY TOUCH RANDOMKEY SORT DUMP EXPIRE PEXPIRE PEXPIREAT TTL PTTL PERSIST EXPIRETIME PEXPIRETIME DBSIZE FLUSHDB FLUSHALL OBJECT ENCODING OBJECT REFCOUNT OBJECT IDLETIME MEMORY USAGE LASTSAVE
PUBLISH SUBSCRIBE PSUBSCRIBE UNSUBSCRIBE PUNSUBSCRIBE
MULTI EXEC DISCARD WATCH UNWATCH
EVAL EVALSHA SCRIPT LOAD SCRIPT EXISTS SCRIPT FLUSH
PING ECHO INFO SAVE BGSAVE LASTSAVE TIME WAIT HELLO SELECT RESET QUIT AUTH CONFIG GET CONFIG SET COMMAND COUNT DEBUG SLEEP
CLIENT LIST CLIENT ID CLIENT SETNAME CLIENT GETNAME CLIENT KILL CLIENT INFO SLOWLOG GET SLOWLOG LEN SLOWLOG RESET ACL WHOAMI ACL LIST ACL CAT
REPLICAOF SLAVEOF MULTIMASTER ENABLE MULTIMASTER DISABLE MULTIMASTER STATUS MULTIMASTER PEERS
CLUSTER NODES CLUSTER SLOTS CLUSTER MEET CLUSTER FORGET CLUSTER INFO CLUSTER MYID CLUSTER RESET CLUSTER KEYSLOT CLUSTER COUNTKEYSINSLOT CLUSTER GETKEYSINSLOT CLUSTER REPLICATE CLUSTER FAILOVER CLUSTER SAVECONFIG
RATELIMIT RATELIMIT.PEEK RATELIMIT.RESET SETDELAYED ENQUEUE DEQUEUE QACK QNACK QPEEK QLEN QINFO QDEAD JSON.SET JSON.GET JSON.DEL JSON.TYPE JSON.NUMINCRBY JSON.ARRAPPEND JSON.ARRLEN JSON.KEYS TAG TAGGET TAGDEL TAGQUERY TS.ADD TS.RANGE TS.GET TS.INFO TS.DOWNSAMPLE HSEARCH CRON.ADD CRON.DEL CRON.LIST CRON.INFO AUDIT AUDIT.SEARCH AUDIT.ENABLE AUDIT.DISABLE AUDIT.CLEAR AUDIT.SIZE SCHEMA.SET SCHEMA.GET SCHEMA.DEL SCHEMA.LIST WEBHOOK REGISTER WEBHOOK LIST WEBHOOK UNREGISTER NAMESPACE CREATE NAMESPACE USE NAMESPACE LIST NAMESPACE CURRENT NAMESPACE DROP
These are accepted so clients don't break, but they don't do much:
LATENCY LATEST LATENCY HISTORY LATENCY RESET ACL GETUSER MODULE LIST SWAPDB (error -- use namespaces) RESTORE (error -- not implemented)
| Flag | Default | Description |
|---|---|---|
--addr |
127.0.0.1:7379 |
RESP listen address |
--http-addr |
(off) | HTTP API address |
--appendonly |
true |
AOF persistence |
--appendfilename |
vole.aof |
AOF file path |
--appendfsync |
everysec |
AOF fsync policy |
--snapshot |
vole.rdb.json |
Snapshot file path |
--snapshot-interval |
0 |
Snapshot interval (e.g. 5m) |
--maxmemory |
0 |
Memory limit in bytes |
--maxmemory-policy |
noeviction |
Eviction policy |
--requirepass |
(none) | Client password |
--tls-cert |
(none) | TLS certificate |
--tls-key |
(none) | TLS private key |
--replicaof |
(none) | Leader address (leader-follower mode) |
--multimaster |
false |
Enable multi-master replication |
--node-id |
(auto) | Cluster node ID |
--peers |
(none) | Cluster peers |
Pure Go. The go.mod has no dependencies. go build gives you a static binary you can drop anywhere.
Under the hood: read/write lock separation so concurrent reads don't step on each other. Lists use a ring-buffer deque, so push and pop are O(1) from either end. Sorted sets stay sorted on every write, which means range queries never need to re-sort. Stream reads use binary search. Every AOF entry carries a CRC32 checksum.
About 23,000 lines across 34 files. 125 tests, 26 benchmarks.
When you shut it down (Ctrl-C or SIGTERM), Vole drains in-flight connections, writes a final snapshot, flushes the AOF, and stops replication before exiting.
Vole is open source. If you want to get involved -- whether that's a bug fix, a new command, better docs, or just telling us about a problem you ran into -- we'd genuinely appreciate it.
CONTRIBUTING.md has everything you need: how to build, how the code is organized, and what a good pull request looks like.
We also have a Code of Conduct. The gist: be decent to each other.
Vole is licensed under the BSD 3-Clause License. See LICENSE for the full text.