a low-latency, memory-efficient, distributed cache written in Rust. designed to outperform Redis on throughput, latency, and memory efficiency while keeping the codebase small and readable.
- resp3 protocol — full compatibility with
redis-cliand existing Redis clients - string commands — GET, SET (with NX/XX/EX/PX), MGET, MSET, MSETNX, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT, APPEND, STRLEN, GETRANGE, SETRANGE, GETSET, GETDEL, GETEX
- list operations — LPUSH, RPUSH, LPOP, RPOP, LRANGE, LLEN, LINDEX, LSET, LTRIM, LINSERT, LREM, LPOS, LMOVE, LMPOP, BLPOP, BRPOP
- sorted sets — ZADD (with NX/XX/GT/LT/CH), ZREM, ZSCORE, ZRANK, ZREVRANK, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZINCRBY, ZCOUNT, ZPOPMIN, ZPOPMAX, ZCARD, ZSCAN, ZUNION, ZINTER, ZDIFF, ZUNIONSTORE, ZINTERSTORE, ZDIFFSTORE, ZMPOP, ZRANDMEMBER
- hashes — HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HINCRBY, HINCRBYFLOAT, HKEYS, HVALS, HMGET, HSCAN, HRANDFIELD
- sets — SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SMISMEMBER, SUNION, SINTER, SDIFF, SUNIONSTORE, SINTERSTORE, SDIFFSTORE, SRANDMEMBER, SPOP, SSCAN, SMOVE, SINTERCARD
- bitmaps — GETBIT, SETBIT, BITCOUNT, BITPOS, BITOP
- key commands — DEL, UNLINK, EXISTS, EXPIRE, EXPIREAT, EXPIRETIME, TTL, PEXPIRE, PEXPIREAT, PEXPIRETIME, PTTL, PERSIST, TYPE, SCAN, KEYS, RENAME, COPY, TOUCH, RANDOMKEY, SORT, OBJECT ENCODING/REFCOUNT, WAIT
- server commands — PING, ECHO, INFO, DBSIZE, FLUSHDB, FLUSHALL, MEMORY USAGE, BGSAVE, BGREWRITEAOF, AUTH, QUIT, CONFIG GET/SET/REWRITE, SLOWLOG, CLIENT ID/SETNAME/GETNAME/LIST, TIME, LASTSAVE, ROLE, MONITOR, COMMAND/COMMAND COUNT/COMMAND INFO/COMMAND DOCS
- transactions — MULTI, EXEC, DISCARD, WATCH/UNWATCH for optimistic locking
- acl — per-user command permissions and key pattern restrictions: ACL SETUSER, GETUSER, DELUSER, LIST, WHOAMI, CAT, USERS
- pub/sub — SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, plus PUBSUB introspection
- vector similarity search — HNSW-backed approximate nearest neighbor search with cosine, L2, and inner product metrics (compile with
--features vector) - protobuf storage — schema-validated protobuf values with field-level access (compile with
--features protobuf) - authentication —
--requirepassfor redis-compatible AUTH (legacy and username/password forms), optional ACL for per-user access control - tls support — redis-compatible TLS on a separate port, with optional mTLS for client certificates
- protected mode — rejects non-loopback connections when no password is set on public binds
- observability — prometheus metrics (
--metrics-port), enriched INFO with 6 sections, SLOWLOG command - sharded engine — shared-nothing, thread-per-core design with no cross-shard locking
- cluster mode — distributed keyspace across multiple nodes: 16384 hash slots, SWIM gossip, Raft-based topology, MOVED/ASK redirects, live slot migration
- active expiration — background sampling cleans up expired keys without client access
- memory limits — per-shard byte-level accounting with configurable limits
- lru eviction — approximate LRU via random sampling when memory pressure hits
- persistence — append-only file (AOF) and point-in-time snapshots
- encryption at rest — optional AES-256-GCM encryption for AOF and snapshot files (compile with
--features encryption) - pipelining — multiple commands per read for high throughput
- interactive CLI —
ember-cliwith REPL, syntax highlighting, tab-completion, inline hints, cluster subcommands, and built-in benchmark - graceful shutdown — drains active connections on SIGINT/SIGTERM before exiting
# docker — no install needed
docker run -p 6379:6379 ghcr.io/kacy/ember:latest
redis-cli ping
# => PONGor with docker compose for a persistent single-node setup:
curl -O https://raw.githubusercontent.com/kacy/ember/main/docker-compose.yml
docker compose up -d
redis-cli pingember is purpose-built for caching. some Redis features are intentionally excluded:
- lua scripting —
EVAL,EVALSHA,SCRIPT *are not implemented and not planned. server-side logic may be supported via WASM extensions in a future release. - streams —
XADD,XREAD, and related commands are not implemented. use a dedicated stream store (Kafka, Redpanda, etc.) for this. - multiple databases —
SELECT,MOVE,SWAPDBare not supported. ember is single-database by design. - bitfield —
BITFIELDandBITFIELD_ROare not planned; use application-level serialization instead.
see docs/compatibility.md for the full command support matrix.
- RENAME requires the source and destination keys to hash to the same shard. cross-shard renames return an error.
- transactions (MULTI/EXEC) are fully atomic within a single shard. cross-shard transactions execute in order but are not globally atomic — the same limitation as Redis Cluster.
- geo commands are coming in a future release.
redis-cliconnects on port 6379 (RESP3). the typed gRPC API is on port 6380 (when--features grpcis compiled in).
homebrew (macOS/Linux)
brew tap kacy/ember
brew install ember-dbthis installs both ember-server and ember-cli.
install script
curl -fsSL https://emberdb.com/install | bashinstalls the latest ember-server and ember-cli to /usr/local/bin. useful flags:
# server only (skip ember-cli)
curl -fsSL https://emberdb.com/install | bash -s -- --server-only
# pin to a specific version
... | bash -s -- --version v0.4.6
# custom install directory
... | bash -s -- --prefix ~/.local/binfrom source
git clone https://github.com/kacy/ember
cd ember
cargo build --release# build server and cli
cargo build --release
# run the server (defaults to 127.0.0.1:6379)
./target/release/ember-server
# with memory limit and eviction
./target/release/ember-server --max-memory 256M --eviction-policy allkeys-lru
# with persistence
./target/release/ember-server --data-dir ./data --appendonly
# with encryption at rest (requires --features encryption)
./target/release/ember-server --data-dir ./data --appendonly \
--encryption-key-file /path/to/keyfile
# with TLS (runs alongside plain TCP)
./target/release/ember-server --tls-port 6380 \
--tls-cert-file cert.pem --tls-key-file key.pemember speaks RESP3, so redis-cli works as a drop-in replacement — but ember-cli adds syntax highlighting, tab-completion, inline hints, and auto-reconnect.
# ember-cli: interactive REPL with autocomplete and help
ember-cli # starts REPL at 127.0.0.1:6379>
ember-cli -H 10.0.0.1 -p 6380 # connect to a different host
ember-cli -a mypassword # authenticate
# one-shot mode
ember-cli SET hello world # => OK
ember-cli GET hello # => "hello"
# cluster management subcommands
ember-cli cluster info # cluster state
ember-cli cluster nodes # list nodes
ember-cli cluster meet 10.0.0.1 6379
# built-in benchmark
ember-cli benchmark -n 100000 -c 50 -P 16
# redis-cli works too — same protocol, same port
redis-cli SET hello world # => OK
redis-cli GET hello # => "world"# everything below works with either ember-cli or redis-cli
SET counter 10
INCR counter # => (integer) 11
LPUSH mylist a b c # => (integer) 3
LRANGE mylist 0 -1 # => 1) "c" 2) "b" 3) "a"
ZADD board 100 alice 200 bob
ZRANGE board 0 -1 WITHSCORES
HSET user:1 name alice age 30
HGETALL user:1 # => 1) "name" 2) "alice" 3) "age" 4) "30"
SADD tags rust cache fast # => (integer) 3
SMEMBERS tags # => 1) "cache" 2) "fast" 3) "rust"
SET temp data EX 60
TTL temp # => 59
SCAN 0 MATCH "user:*" COUNT 100
DBSIZE # => (integer) 6
# blocking list ops (work queues)
BLPOP tasks 5 # blocks up to 5 seconds for an element
# in another terminal: LPUSH tasks job1 => unblocks with ["tasks", "job1"]
# transactions
MULTI
SET account:a 100
SET account:b 200
EXEC # => [OK, OK] — atomic batch
# TLS
ember-cli -p 6380 --tls --tls-insecure PINGember distributes keys across 16384 hash slots using CRC16. each node owns a non-overlapping range of slots. gossip (SWIM) propagates membership and health; Raft commits topology changes; MOVED and ASK redirects keep clients pointing at the right node during normal operation and live slot migration.
the fastest way to get a 3-node cluster running locally:
make clusterthis builds the release binaries if needed, starts nodes on ports 6379–6381, joins them together, and assigns all 16384 hash slots evenly. you should see something like:
starting node 1 (port 6379, bootstrap)...
starting node 2 (port 6380)...
starting node 3 (port 6381)...
node :6379 ready
node :6380 ready
node :6381 ready
joining cluster...
assigning slots...
cluster_state:ok
cluster_slots_assigned:16384
cluster_known_nodes:3
cluster_size:3
to start nodes manually:
# node A — bootstrap a new cluster and own all slots initially
./target/release/ember-server --port 6379 --cluster-enabled --cluster-bootstrap \
--data-dir ./data/node-a
# node B and C — join with --cluster-enabled, then CLUSTER MEET into A
./target/release/ember-server --port 6380 --cluster-enabled --data-dir ./data/node-b
./target/release/ember-server --port 6381 --cluster-enabled --data-dir ./data/node-c
# join B and C into the cluster
ember-cli -p 6380 cluster meet 127.0.0.1 6379
ember-cli -p 6381 cluster meet 127.0.0.1 6379
# redistribute slots (5461 slots each after rebalancing)
ember-cli -p 6379 cluster addslots $(seq 0 5460 | tr '\n' ' ')
ember-cli -p 6380 cluster addslots $(seq 5461 10922 | tr '\n' ' ')
ember-cli -p 6381 cluster addslots $(seq 10923 16383 | tr '\n' ' ')connect to any node and start writing:
ember-cli -p 6379 SET hello world # => OK
ember-cli -p 6380 GET hello # => "world" or MOVED redirect
ember-cli -p 6379 cluster info # cluster_state:ok, 16384 slots assigned
ember-cli -p 6380 cluster nodes # 3 nodes, each with correct slot rangeclients that support cluster mode (including redis-cli --cluster) follow MOVED redirects automatically. ember-cli routes each command to the correct node based on the key's CRC16 hash slot.
stop the cluster and clean up:
make cluster-stop # SIGTERM the nodes, keep data
make cluster-clean # stop + delete ./data/cluster/| command | description |
|---|---|
CLUSTER INFO |
cluster state, slot counts, node count |
CLUSTER NODES |
list all known nodes with their slot ranges |
CLUSTER SLOTS |
slot-to-node mapping in array form |
CLUSTER KEYSLOT key |
return the hash slot for a key |
CLUSTER MYID |
return this node's ID |
CLUSTER MEET host port |
join another node into the cluster |
CLUSTER ADDSLOTS slot [slot ...] |
assign slots to this node |
CLUSTER DELSLOTS slot [slot ...] |
remove slot assignments from this node |
CLUSTER FORGET node-id |
remove a node from the cluster view |
CLUSTER SETSLOT slot IMPORTING|MIGRATING|NODE|STABLE ... |
manage slot migration state |
CLUSTER COUNTKEYSINSLOT slot |
count keys owned by a slot on this node |
CLUSTER GETKEYSINSLOT slot count |
list keys in a slot (used during migration) |
MIGRATE host port key db timeout [COPY] [REPLACE] |
move a key to another node |
RESTORE key ttl payload |
receive a migrated key |
ASKING |
tell the server to honor an ASK redirect for the next command |
replication commands (REPLICATE, FAILOVER) are implemented — see the status table below.
see ARCHITECTURE.md for how clustering works under the hood (raft, gossip, hash slots, migration).
ember can store schema-validated protobuf messages and access individual fields server-side. compile with --features protobuf to enable.
# build with protobuf support
cargo build --release --features protobufcommands:
| command | description |
|---|---|
PROTO.REGISTER name <descriptor> |
register a compiled FileDescriptorSet |
PROTO.SET key type_name <data> [EX s] [PX ms] [NX|XX] |
store a validated protobuf value |
PROTO.GET key |
retrieve the full encoded message |
PROTO.TYPE key |
return the message type name |
PROTO.SCHEMAS |
list all registered schema names |
PROTO.DESCRIBE name |
list message types in a schema |
PROTO.GETFIELD key field_path |
read a single field (dot-separated nested paths) |
PROTO.SETFIELD key field_path value |
update a single scalar field |
PROTO.DELFIELD key field_path |
clear a field to its default value |
field-level operations decode/mutate/re-encode on the server, so clients don't need protobuf libraries for simple reads and writes. nested paths use dot notation (e.g., address.city). complex types (repeated, map, nested messages) require PROTO.GET/PROTO.SET for full replacement.
ember supports HNSW-backed approximate nearest neighbor search for building recommendation systems, semantic search, and RAG pipelines. compile with --features vector to enable.
# build with vector support
cargo build --release --features vectorcommands:
| command | description |
|---|---|
VADD key element f32 [f32 ...] [METRIC COSINE|L2|IP] [QUANT F32|F16|Q8] [M n] [EF n] |
add a vector to the set |
VADD_BATCH key DIM n elem1 f32... elem2 f32... [METRIC|QUANT|M|EF] |
add multiple vectors in one command |
VSIM key f32 [f32 ...] COUNT k [EF n] [WITHSCORES] |
k nearest neighbors |
VREM key element |
remove a vector |
VGET key element |
retrieve stored vector values |
VCARD key |
number of vectors in the set |
VDIM key |
dimensionality of the vector set |
VINFO key |
metadata: dim, count, metric, quantization, M, ef |
index configuration (METRIC, QUANT, M, EF) is set on the first VADD and locked after that. dimension is inferred from the first vector's length. each key owns its own independent HNSW index.
# store some embeddings
VADD docs doc1 0.1 0.2 0.3 METRIC COSINE
VADD docs doc2 0.9 0.1 0.0
VADD docs doc3 0.0 0.8 0.2
# find 2 nearest neighbors
VSIM docs 0.1 0.3 0.2 COUNT 2 WITHSCORES
# => 1) "doc1" 2) "0.05" 3) "doc3" 4) "0.12"
VCARD docs # => (integer) 3
VDIM docs # => (integer) 3
VINFO docs # => metric, quantization, dim, count, M, efdistance metrics: COSINE (default), L2 (squared euclidean), IP (inner product). quantization: F32 (default), F16 (half precision), Q8 (8-bit integer). lower precision uses less memory at a small accuracy cost.
ember supports a TOML config file, environment variables, and CLI flags. the resolution order is: defaults → config file → env vars → CLI flags (highest priority wins).
# generate a config file with all defaults
ember-server --config-template > ember.toml
# start with a config file
ember-server --config ember.toml
# config file + CLI override
ember-server --config ember.toml --port 7777
# env vars use the EMBER_ prefix
EMBER_PORT=8888 ember-server --config ember.tomlsee ember.example.toml for the full annotated config with all available options.
runtime changes via CONFIG SET persist in memory. use CONFIG REWRITE to flush them back to the config file.
| flag | default | description |
|---|---|---|
--config / -c |
— | path to TOML config file |
--config-template |
— | print default config to stdout and exit |
--host |
127.0.0.1 | address to bind to |
--port |
6379 | port to listen on |
--shards |
CPU cores | number of worker threads (shards) |
--max-memory |
unlimited | memory limit (e.g., 256M, 1G) |
--eviction-policy |
noeviction | noeviction or allkeys-lru |
--data-dir |
— | directory for persistence files |
--appendonly |
false | enable append-only file logging |
--appendfsync |
everysec | fsync policy: always, everysec, no |
--metrics-port |
— | prometheus /metrics and /health HTTP port |
--slowlog-log-slower-than |
10000 | log commands slower than N microseconds (-1 disables) |
--slowlog-max-len |
128 | max entries in slow log ring buffer |
--requirepass |
— | require AUTH with this password before running commands |
--tls-port |
— | port for TLS connections (enables TLS when set) |
--tls-cert-file |
— | path to server certificate (PEM) |
--tls-key-file |
— | path to server private key (PEM) |
--tls-ca-cert-file |
— | path to CA certificate for client verification |
--tls-auth-clients |
no | require client certificates (yes or no) |
--encryption-key-file |
— | path to 32-byte key file for AES-256-GCM encryption at rest (requires --features encryption) |
--cluster-enabled |
false | enable cluster mode with gossip-based discovery and slot routing |
--cluster-bootstrap |
false | bootstrap a new cluster as a single node owning all 16384 slots |
--cluster-port-offset |
10000 | port offset for the cluster gossip bus (data_port + offset) |
--cluster-node-timeout |
5000 | node timeout in milliseconds for failure detection |
make check # fmt, clippy, tests
make build # debug build
make release # release build
make test # run all tests
make docker-build # build docker image
make helm-lint # validate helm chart
make helm-template # render helm templatessee CONTRIBUTING.md for development workflow and code standards.
deploy to kubernetes with Helm:
# add the helm repo
helm repo add ember https://charts.emberdb.com
# install with defaults (single replica, ClusterIP service)
helm install ember ember/ember
# install with custom settings
helm install ember ember/ember \
--set ember.maxMemory=512M \
--set ember.evictionPolicy=allkeys-lru \
--set ember.appendonly=true
# connect via port-forward
kubectl port-forward svc/ember 6379:6379
ember-clisee helm/ember/values.yaml for all configurable values.
crates/
ember-server/ main server binary
ember-core/ core engine (keyspace, types, sharding)
ember-protocol/ RESP3 wire protocol
ember-persistence/ AOF and snapshot durability
ember-cluster/ raft consensus, gossip, slot management, migration
ember-cli/ interactive CLI client (REPL, cluster subcommands, benchmark)
tested on GCP c2-standard-8 (8 vCPU Intel Xeon @ 3.10GHz). see bench/README.md for full results.
| vs redis | vs dragonfly | |
|---|---|---|
| SET | 1.9x faster | 2.2x faster |
| GET | 1.8x faster | 2.2x faster |
redis-benchmark, 64B values, P=16, 8 threads. take these comparisons with a grain of salt — ember is a small indie project; Redis and Dragonfly are battle-tested systems built by large teams over many years. see bench/README.md for important caveats.
highlights:
- 1.79M SET/sec, 2.00M GET/sec (redis-benchmark, 64B, P=16)
- p99 latency: 1.25ms SET, 1.17ms GET (P=1)
- vector insert: 5.5k vectors/sec (8-shard), 2.4k vectors/sec (single key); query: 1.8k queries/sec, p99=0.62ms
- vector memory: 4-6x less than chromadb/pgvector/qdrant (29-31 MB vs 139-178 MB for 100k vectors)
- memory: 180 bytes/key for strings, 215 bytes/key for hashes (redis: 173/170 bytes/key)
./bench/bench-quick.sh # quick sanity check
./bench/compare-redis.sh # redis-benchmark comparison
./bench/bench-memtier.sh # memtier_benchmark comparison
./bench/bench-vector.sh # vector similarity (ember vs chromadb vs pgvector vs qdrant)
./bench/bench-grpc.sh # gRPC vs RESP3
./bench/bench-all.sh # run everything190+ commands, 1,690+ tests, ~25k lines of code (~47k including tests and comments). see bench/README.md for full methodology and results.
ember uses a thread-per-core architecture with channel-based routing. each shard owns a partition of the keyspace with no cross-shard locking on the hot path. supports all data types and enables atomic multi-key operations.
cluster mode (--cluster-enabled): multiple ember nodes form a single distributed cache. the keyspace is partitioned into 16384 hash slots using CRC16 — each node owns a non-overlapping range. SWIM gossip propagates membership and health across nodes; topology changes (adding nodes, reassigning slots) go through a Raft consensus group so the cluster view is always consistent. clients are routed via MOVED redirects when a key lands on a different node, and ASK redirects during live slot migration.
contributions welcome — see CONTRIBUTING.md.
| guide | description |
|---|---|
| redis compatibility | command-by-command support matrix |
| migrating from redis | config mapping, behavioral differences, step-by-step migration |
| performance tuning | shard count, pipeline depth, memory tuning |
| production checklist | OS tuning, memory limits, persistence, monitoring, security |
| troubleshooting | common issues and solutions |
| client libraries | using existing Redis clients (ioredis, go-redis, redis-py, etc.) with Ember |
| architecture deep dive | execution model, protocol layer, data model, persistence, clustering |
| changelog | release history and phase summaries |
| security policy | vulnerability reporting, deployment hardening |
see SECURITY.md for:
- reporting vulnerabilities
- security considerations for deployment
- recommended configuration
note: use --requirepass to enable authentication. protected mode is active by default when no password is set, rejecting non-loopback connections on public binds. for encryption at rest, see --encryption-key-file — key loss means data loss, so back up your key file separately from your data.
Apache 2.0
