kevy-embedded
The kevy key–value engine as a Rust library — same data structures, same
commands, no network. Drop it into a binary and call Store directly.
- Pure Rust, zero
crates.iodependencies. - All five Redis data types plus bitmaps, pub/sub, TTL, and three transaction shapes.
- Snapshot + append-only-file persistence, eight eviction policies.
- Builds for
wasm32-unknown-unknownandwasm32-wasip1.
use ;
let store = open?;
store.set?;
assert_eq!;
# Ok::
Install
When to use
- In-process cache or store. A Redis-shaped LRU/LFU that handles bytes, hashes, lists, sets, sorted sets, bitmaps, and TTL.
- Embedded persistent KV.
Config::default().with_persist("./data")enables snapshot + AOF and the next process boot resumes where the last one stopped. - WebAssembly target. No threads, no OS sockets — set
Config::with_ttl_reaper_manual()and callStore::tick()from your event loop. Walkthrough indocs/wasm.md.
When NOT to use
- Cross-process access.
kevy-embeddedis single-process. Use thekevyserver when more than one process needs to share state. - Distributed consistency or replication you control. Embed-as- read-replica exists (see further down) but the writer is still a single kevy server. For multi-writer, multi-region, transactional consistency, pick a real distributed database.
Quick start
Strings
use ;
let store = open?;
store.set?;
assert_eq!;
store.incr?;
store.incr_by?;
assert_eq!;
store.append?;
store.append?;
assert_eq!;
# Ok::
Atomic single-call helpers: getset, getdel, setnx, setrange,
getrange, decr, decr_by, incrbyfloat, mset, mget.
Hashes
use ;
let store = open?;
store.hset?;
assert_eq!;
assert_eq!;
assert!;
let all: = store.hgetall?;
let some = store.hmget?;
store.hincrby?;
store.hsetnx?;
# Ok::
Lists
use ;
let store = open?;
store.rpush?;
assert_eq!;
let head = store.lpop?;
assert_eq!;
let window: = store.lrange?;
assert_eq!;
# Ok::
Plus lindex, linsert, lrem, lset, ltrim, and the blocking
variants when running inside a server.
Sets
use ;
let store = open?;
store.sadd?;
assert_eq!;
assert!;
store.sadd?;
store.sadd?;
let inter = store.sinter?;
assert_eq!;
# Ok::
Sorted sets
use ;
let store = open?;
// Note: (score, member) tuple order.
store.zadd?;
assert_eq!;
assert_eq!;
let top: = store.zrevrange?;
assert_eq!;
store.zincrby?;
# Ok::
Range queries: zrange, zrevrange, zrange_by_score,
zrev_range_by_score, zcount, zpopmin, zremrangebyrank,
zremrangebyscore.
Bitmaps
use ;
let store = open?;
store.setbit?;
assert_eq!;
assert_eq!;
store.setbit?;
store.setbit?;
store.setbit?;
store.bitop?;
# Ok::
Plus bitpos, getrange, and setrange for byte-aligned slice work.
TTL
use ;
use Duration;
let store = open?;
store.set?;
store.expire?;
store.pexpire?; // 30 seconds in ms
store.expireat?; // absolute unix-second
assert!;
let ttl_s = store.ttl_secs; // -1 no TTL, -2 absent
// Atomic get + (re)set TTL in one call.
let val = store.getex?;
# Ok::
In-process pub/sub
use ;
let store = open?;
let publisher = store.clone;
let mut sub = store.subscribe;
let _ack = sub.recv?; // drain the SUBSCRIBE ack
publisher.publish;
match sub.recv?
# Ok::
Channel and pattern (PSUBSCRIBE-style glob) subscriptions are both
supported. Dropping the Subscription unsubscribes from every channel
atomically.
Cursor-based scan
use ;
let store = open?;
for i in 0..1_000u32
let user_keys: = store
.keys_iter
.collect;
assert_eq!;
# Ok::
The keys_iter, hash_iter, and zset_iter wrappers turn the raw
SCAN / HSCAN / ZSCAN cursors into ordinary Rust iterators.
Three transaction shapes — which to pick
kevy-embedded exposes three commit shapes. They differ on what they
guarantee, not on what they let you write inside the closure.
| Shape | Atomicity | Cross-shard | Fsync per commit | Use when |
|---|---|---|---|---|
Store::atomic |
All-or-nothing | Single shard (all keys share a hashtag) | One | One key or one hashtag group. Highest throughput. |
Store::atomic_all_shards |
All-or-nothing | All shards | One | A read-modify-write that spans multiple unrelated keys. |
Store::pipeline |
Per-op | Any | One per batch | High-throughput write streams where the app is fine with per-op failure. |
atomic — single-shard atomic closure
use ;
let store = open?;
// Both keys share the {user:42} hashtag → same shard.
let result = store.atomic?;
assert_eq!;
# Ok::
atomic_all_shards — multi-shard atomic closure
use ;
let store = open?;
store.atomic_all_shards?;
# Ok::
Acquires every shard lock in deterministic order, so it is heavier than
atomic and should be used only when the closure genuinely needs more
than one shard.
pipeline — non-atomic batched writes
use ;
let store = open?;
let mut p = store.pipeline;
for i in 0..1000
let replies = p.execute?; // one fsync, 1000 entries
assert_eq!;
# Ok::
Each command commits independently, so a single command failing does
not roll back its neighbours. One fsync at the end of execute()
amortises the cost across the batch.
Persistence
Config::default().with_persist(dir) enables both snapshot and AOF.
Store::open first loads the snapshot, then replays the AOF, so a
fresh process resumes exactly where the previous one left off.
use ;
let store = open?;
# Ok::
AppendFsync |
Max data loss on crash | Throughput vs EverySec |
|---|---|---|
Always |
0 bytes | ~50% |
EverySec (default) |
≤ 1 second | baseline |
No |
up to ~30 s (kernel pagecache flush) | slightly faster |
Compaction:
Store::save_snapshot()writes a full snapshot synchronously (equivalent ofSAVE).Store::rewrite_aof()rebuilds a compact AOF from current state and atomically swaps it in (equivalent ofBGREWRITEAOF).
Eviction
use ;
let store = open?;
# Ok::
All eight Redis policies are supported: NoEviction (default),
AllKeysLru, AllKeysLfu, AllKeysRandom, VolatileLru,
VolatileLfu, VolatileRandom, VolatileTtl. LRU and LFU use Redis-
compatible 24-bit clock + sample-based selection.
Under NoEviction a write that would exceed max_memory returns the
standard Redis OOM error before it runs. Shrinking verbs (DEL,
LPOP, SREM, EXPIRE, FLUSH*) always succeed so the instance is
always recoverable.
Thread safety
Store methods take &self, and Store is Clone — each clone is an
Arc bump that reaches the same keyspace, AOF, reaper, and pub/sub
bus. The reaper joins and the AOF flushes exactly once when the last
clone drops.
use ;
let store = open?;
let s2 = store.clone;
spawn;
# Ok::
For multi-core scale where a single mutex would dominate, use the
kevy server, which shards the
keyspace across cores with no shared lock.
Join a kevy server cluster from your own process
Three deployment shapes, all backed by the same Store API.
Pure embed — no network
The default. Reads and writes hit the in-process keyspace.
use ;
let store = open?;
store.set?;
# Ok::
Embed as a read replica
Subscribe to a kevy server primary's replication stream. Every applied
mutation flows into the in-process Store over RESP. Local reads pay
zero network round-trip; local writes return READONLY.
use Store;
let store = open_replica?;
let v: = store.get?;
assert!;
// store.set(b"k", b"v") → Err(READONLY)
# Ok::
Use the tunable form when you need a stable replica id (so the primary reuses your backlog after a quick restart) or a custom reconnect window:
use ;
use Duration;
let store = open?;
# Ok::
Embed as a scoped writer
The cluster declares per-prefix writer ownership on the server side
([cluster] scopes = "app:billing:=embed-a" in the server's TOML). An
embed process that owns a prefix writes locally, while wrong-prefix
writes anywhere in the cluster are redirected with -MISDIRECTED writer is <host:port>. See docs/cluster.md
for the server-side TOML and the MOVE-SCOPE migration protocol.
URL facade — same code switches between embed and server
kevy-client accepts both mem:// (in-process via kevy-embedded)
and kevy://host:port (TCP):
use Connection;
let url = var
.unwrap_or_else;
let mut conn = open?;
conn.set?;
# Ok::
A single Rust binary can run as a server, as a pure embedded library, or as a hybrid (embed-as-replica + remote primary) — the calling code never branches on transport.
Out of scope
kevy-embedded deliberately omits:
- Multi-database
SELECT. Single keyspace perStore. - AUTH and ACL. Single trust domain — the calling process.
EVAL/SCRIPT. The Lua scripting bridge ships in thekevyserver crate.- Cluster mode commands.
CLUSTER SLOTS / SHARDS / NODESbelong on the server side; the embedded library is single-process.
Maintenance hooks
For very long-running embedded use:
# use ;
# let store = open?;
store.tick; // active TTL reaper
store.save_snapshot?; // RDB-style dump for restart speed
store.rewrite_aof?; // compact AOF, drop redundant writes
# Ok::
When running under Config::with_ttl_reaper_manual() (WASM, single-
threaded host), tick() is the only path through which expired keys
are reaped.
Examples in the repository
examples/embedded.rs— minimum-viable CRUD.examples/embedded-cache.rs— hard-cap LRU cache.examples/embed_vs_server.rs— same Rust caller against in-process, kevy server, valkey, redis.
Dependencies
Zero crates.io dependencies. The only crates pulled in are kevy's
own kevy-store, kevy-persist, kevy-hash, and kevy-replicate —
all path-deps inside the workspace. The network reactor crates
(kevy-rt, kevy-sys, kevy-uring) are intentionally not pulled,
so kevy-embedded compiles for any target kevy-store + kevy-persist
compile for, including wasm32-unknown-unknown and wasm32-wasip1.
License
MIT OR Apache-2.0, at your option.