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, and KEYS skip it until it becomes active.
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 runs Redis-style Lua scripts with redis.call()/redis.pcall(), KEYS, ARGV, script cache commands, read-only variants, and Redis Functions support.
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. Followers track replication offsets and can resume from the backlog when the leader still has the missing range; otherwise they fall back to a fresh snapshot. Coordinated failover is available through the cluster failover commands.
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.
Vole returns standard MOVED redirects for keys owned by another node, and compatible clients route the command to the owner. For slot ownership changes, pair the metadata update with replication, multi-master, or an application-level migration plan so data stays available throughout the topology change.
# 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, per-command latency and error counters, connections, keyspace hit/miss rate, memory usage, GC stats, goroutine count, uptime, eviction count, lazy-free queue depth, store lock wait time, and command key-stripe wait time.
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.
Vole keeps an approximate memory estimate per key and updates it on mutation, so eviction decisions do not depend on periodic full keyspace rescans. UNLINK removes keys from the live store immediately and releases larger detached containers from a background lazy-free worker; DEL remains synchronous.
GET SET MGET MSET MSETNX MSETEX SETNX SETEX PSETEX GETSET GETEX GETDEL GETRANGE SETRANGE SUBSTR INCR INCRBY DECR DECRBY INCRBYFLOAT APPEND STRLEN SETBIT GETBIT LCS DIGEST DELEX
HSET HMSET HGET HMGET HSTRLEN HGETALL HDEL HEXISTS HKEYS HVALS HLEN HINCRBY HINCRBYFLOAT HSETNX HRANDFIELD
LPUSH RPUSH LPUSHX RPUSHX LPOP RPOP LRANGE LLEN LINDEX LSET LINSERT LPOS LREM BLPOP BRPOP RPOPLPUSH BRPOPLPUSH LMOVE BLMOVE LMPOP
SADD SREM SMEMBERS SISMEMBER SMISMEMBER SCARD SRANDMEMBER SMOVE SPOP SINTER SINTERCARD SINTERSTORE SUNION SUNIONSTORE SDIFF SDIFFSTORE
ZADD ZRANGE ZRANGEBYSCORE ZREVRANGEBYSCORE ZRANGEBYLEX ZLEXCOUNT ZREVRANGE ZRANGESTORE ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE ZREMRANGEBYLEX ZSCORE ZMSCORE ZCARD ZRANK ZREVRANK ZCOUNT ZINTERCARD ZPOPMIN ZPOPMAX BZPOPMIN BZPOPMAX ZMPOP ZINCRBY ZUNION ZINTER ZUNIONSTORE ZINTERSTORE ZDIFF ZDIFFSTORE ZRANDMEMBER
PFADD PFCOUNT PFMERGE
XADD XDEL XRANGE XREVRANGE XREAD XLEN XTRIM XINFO XINFO HELP XGROUP CREATE XGROUP HELP XREADGROUP XREADGROUP NOACK XACK XCLAIM XAUTOCLAIM XPENDING
GEOADD GEOHASH GEOPOS GEODIST GEOSEARCH GEOSEARCH BYBOX GEOSEARCHSTORE GEORADIUS GEORADIUSBYMEMBER
SETBIT GETBIT BITCOUNT BITOP BITPOS
DEL UNLINK EXISTS TYPE KEYS SCAN RENAME RENAMENX COPY TOUCH RANDOMKEY SORT DUMP RESTORE EXPIRE PEXPIRE PEXPIREAT TTL PTTL PERSIST EXPIRETIME PEXPIRETIME DBSIZE FLUSHDB FLUSHALL OBJECT ENCODING OBJECT REFCOUNT OBJECT IDLETIME OBJECT FREQ OBJECT HELP MEMORY USAGE LASTSAVE
DUMP and RESTORE use Redis RDB-compatible payloads for strings, hashes, lists, sets, and sorted sets. Vole-only values such as streams, HyperLogLog, JSON, and time-series samples use Vole's protected portable envelope instead, because those values do not have a native Redis RDB object representation in this server.
PUBLISH SPUBLISH SUBSCRIBE SSUBSCRIBE PSUBSCRIBE UNSUBSCRIBE SUNSUBSCRIBE PUNSUBSCRIBE
MULTI EXEC DISCARD WATCH UNWATCH
EVAL EVALSHA EVAL_RO EVALSHA_RO SCRIPT LOAD SCRIPT EXISTS SCRIPT FLUSH SCRIPT KILL SCRIPT HELP FUNCTION LOAD FUNCTION LIST FUNCTION STATS FUNCTION DUMP FUNCTION RESTORE FUNCTION DELETE FUNCTION FLUSH FCALL FCALL_RO
PING ECHO INFO SAVE BGSAVE LASTSAVE TIME WAIT WAITAOF ROLE HELLO SELECT RESET QUIT AUTH READONLY READWRITE CONFIG GET CONFIG SET CONFIG RESETSTAT CONFIG REWRITE CONFIG HELP COMMAND COMMAND COUNT COMMAND LIST COMMAND INFO COMMAND DOCS COMMAND GETKEYS COMMAND GETKEYSANDFLAGS COMMAND HELP DEBUG SLEEP DEBUG SET-ACTIVE-EXPIRE DEBUG OBJECT DEBUG HELP LOLWUT
CLIENT LIST CLIENT ID CLIENT SETNAME CLIENT GETNAME CLIENT GETREDIR CLIENT SETINFO CLIENT KILL CLIENT INFO CLIENT REPLY CLIENT TRACKING CLIENT TRACKINGINFO CLIENT CACHING CLIENT NO-EVICT CLIENT NO-TOUCH CLIENT UNBLOCK CLIENT HELP PUBSUB CHANNELS PUBSUB NUMSUB PUBSUB NUMPAT SLOWLOG GET SLOWLOG LEN SLOWLOG RESET SLOWLOG HELP LATENCY LATEST LATENCY HISTORY LATENCY RESET LATENCY DOCTOR LATENCY GRAPH MEMORY USAGE MEMORY STATS MEMORY MALLOC-STATS MEMORY PURGE MEMORY DOCTOR ACL WHOAMI ACL LIST ACL CAT ACL GETUSER ACL SETUSER ACL DELUSER ACL USERS ACL GENPASS ACL DRYRUN ACL SAVE ACL LOAD ACL LOG ACL HELP
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 CLUSTER HELP
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 module introspection commands are available for client compatibility:
MODULE LIST MODULE HELP
Vole provides broad Redis protocol and command compatibility together with slot ownership, redirection, health checks, replication modes, and coordinated/manual failover primitives. For production cluster changes, plan slot moves alongside replication or multi-master topology so clients continue to reach the node that owns each key.
| 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.