meebis 0.5.0

A fast, disposable, in-memory Redis-compatible server for ephemeral dev work
meebis-0.5.0 is not a library.

meebis

A fast, disposable, in-memory Redis-compatible server in Rust — for ephemeral local work.

Spin one up per git worktree, point a couple of processes at it, then throw it away. It boots clean every time, keeps everything in RAM, and forgets it all on exit. There is no persistence, no config file, and nothing to clean up.

  • Fast — matches real Redis throughput (~110–130k ops/sec single-threaded, sub-millisecond latency).
  • Tiny — a sub-1 MB binary using ~2 MB RAM per instance idle, so you can run dozens at once without noticing (see Footprint & performance).
  • Compatible — speaks RESP2 and RESP3 and a broad slice of the Redis command surface. redis-cli, redis-py, and other standard client libraries just work, verified byte-for-byte against Redis 7.2.
  • Disposable — clean on boot, gone on exit. No durability, by design.

It is not a Redis replacement for production. It's a dev tool.

Install

Homebrew (macOS & Linux):

brew install mileszim/tap/meebis

Shell installer (macOS & Linux) — downloads the right prebuilt binary for your platform:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mileszim/meebis/releases/latest/download/meebis-installer.sh | sh

Cargo:

cargo binstall meebis           # prebuilt binary, no compile (needs cargo-binstall)
cargo install meebis            # build from source (crates.io)

Prebuilt binaries for macOS and Linux (arm64 & x86_64) are attached to every release as .tar.xz archives, with sha256 checksums.

From a local checkout:

cargo build --release           # ./target/release/meebis
cargo install --path .          # installs `meebis` into your cargo bin

Run

meebis                          # listen on 127.0.0.1:6379
meebis --port 6400              # pick a port (the main thing you'll configure)
meebis --port 0                 # let the OS choose a free port (printed on boot)
meebis --requirepass hunter2    # require AUTH
meebis 0.1.0 ready on 127.0.0.1:6400 (pid 12345) — in-memory, no persistence

Then connect as you would to Redis:

redis-cli -p 6400 set hello world
redis-cli -p 6400 get hello
import redis
r = redis.Redis(port=6400)      # redis-py, node-redis, go-redis, lettuce, ...
r.set("hello", "world")

Options

Flag Default Description
-p, --port <PORT> 6379 Port to listen on (0 lets the OS pick a free one)
--bind <ADDR> 127.0.0.1 Address to bind
--port-file <PATH> (none) Write the actual listen port to <PATH> on boot
--requirepass <PASS> (none) Require AUTH before most commands
--maxclients <N> 10000 Maximum simultaneous connections
-h, --help / -v, --version Print help / version

Multiple processes can connect to the same instance concurrently and share the keyspace, including pub/sub and transactions.

Discovering the port (one instance per worktree)

meebis is meant to be run one-per-worktree and thrown away. Instead of hand-assigning a port to each worktree, let the OS pick a free one with --port 0 and record it with --port-file, so your app and tests can find it:

meebis --port 0 --port-file .meebis-port
export REDIS_URL="redis://127.0.0.1:$(cat .meebis-port)"

The port is written to the file atomically once meebis has bound, and rewritten on each boot — so a reader never sees a half-written value. A .envrc (direnv) or a Procfile is a natural place to wire this into your dev loop.

Supported commands

Verified byte-for-byte against Redis 7.2 for the cases in the test suite.

  • StringsGET SET (EX/PX/EXAT/PXAT/NX/XX/GET/KEEPTTL) SETNX SETEX PSETEX GETSET GETDEL GETEX APPEND STRLEN INCR DECR INCRBY DECRBY INCRBYFLOAT MGET MSET MSETNX GETRANGE SETRANGE SUBSTR
  • BitmapsSETBIT GETBIT BITCOUNT BITPOS BITOP
  • KeysDEL UNLINK EXISTS EXPIRE PEXPIRE EXPIREAT PEXPIREAT TTL PTTL EXPIRETIME PEXPIRETIME PERSIST KEYS SCAN TYPE RENAME RENAMENX RANDOMKEY TOUCH COPY
  • HashesHSET HMSET HSETNX HGET HMGET HDEL HGETALL HKEYS HVALS HLEN HEXISTS HSTRLEN HINCRBY HINCRBYFLOAT HSCAN HRANDFIELD
  • ListsLPUSH RPUSH LPUSHX RPUSHX LPOP RPOP LLEN LRANGE LINDEX LSET LREM LTRIM LINSERT LPOS RPOPLPUSH LMOVE
  • SetsSADD SREM SMEMBERS SISMEMBER SMISMEMBER SCARD SPOP SRANDMEMBER SMOVE SUNION SINTER SDIFF SUNIONSTORE SINTERSTORE SDIFFSTORE SINTERCARD SSCAN
  • Sorted setsZADD (NX/XX/GT/LT/CH/INCR) ZREM ZSCORE ZMSCORE ZCARD ZCOUNT ZINCRBY ZRANK ZREVRANK ZRANGE ZREVRANGE ZRANGEBYSCORE ZREVRANGEBYSCORE ZRANGEBYLEX ZREVRANGEBYLEX ZLEXCOUNT ZPOPMIN ZPOPMAX BZPOPMIN BZPOPMAX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZSCAN ZRANDMEMBER
  • StreamsXADD (*/<ms>-*/explicit IDs, NOMKSTREAM, MAXLEN/MINID with ~/= and LIMIT) XLEN XRANGE XREVRANGE XREAD (COUNT, BLOCK, $ snapshot) XDEL XTRIM
  • ScriptingEVAL EVALSHA EVAL_RO EVALSHA_RO SCRIPT LOAD/EXISTS/FLUSH, with sandboxed Lua 5.1 (Redis' scripting version), redis.call/pcall/ error_reply/status_reply/sha1hex/log, and the cjson/cmsgpack/bit libraries. Scripts run atomically under a single held keyspace lock — the same guarantee Redis' single thread provides.
  • Pub/SubSUBSCRIBE UNSUBSCRIBE PSUBSCRIBE PUNSUBSCRIBE PUBLISH PUBSUB
  • TransactionsMULTI EXEC DISCARD WATCH UNWATCH
  • ConnectionPING ECHO HELLO AUTH SELECT QUIT RESET CLIENT
  • ServerINFO CONFIG GET/SET DBSIZE FLUSHDB FLUSHALL TIME COMMAND DEBUG OBJECT MEMORY DBSIZE SHUTDOWN LOLWUT (and SAVE, BGSAVE, etc. as accepted no-ops)

Keys and values are binary-safe. EXPIRE and friends work with the full NX/XX/GT/LT option set. Expired keys are removed lazily on access and by a once-per-second sweep.

Deliberately not supported

This is a small dev tool, so some Redis features are intentionally absent:

  • Persistence (RDB/AOF) — everything is in memory and lost on exit.
  • Stream consumer groups (XGROUP, XREADGROUP, XACK, XPENDING, XCLAIM, XAUTOCLAIM) — XADD/XREAD/XRANGE/XTRIM/XDEL are supported; groups are not.
  • List-blocking commands (BLPOP, BRPOP, BLMOVE, BLMPOP, BZMPOP) — BZPOPMIN/BZPOPMAX and XREAD BLOCK are supported; the rest are not yet.
  • HyperLogLog, GEO, and cluster mode.
  • Numbered databasesSELECT is accepted but there is a single shared keyspace. FLUSHDB and FLUSHALL both clear it.

Both RESP2 and RESP3 are supported — clients using either (e.g. redis-py's default RESP3, or redis-cli's RESP2) work without configuration.

WATCH is implemented by fingerprinting watched keys and aborting EXEC if any changed — correct for optimistic-locking patterns, without per-key versioning.

Footprint & performance

meebis is built to be cheap enough to run many instances at once. Measured with a --release build on an Apple Silicon laptop (12 cores):

Metric meebis Notes
Binary size ~860 KB one small binary, stripped
Idle memory (RSS) ~2 MB per instance one OS thread per process
20 instances at once ~40 MB total dozens is a non-issue
Throughput ~110–130k ops/sec redis-benchmark -n 100000 -c 50
Latency ~0.2 ms p50

Command throughput and latency track real Redis 7.2 on the same machine — both execute commands on a single thread — so meebis is not a local-dev bottleneck. A side-by-side redis-benchmark run:

              meebis        redis 7.2
SET       121,212 rps      118,906 rps
GET       114,025 rps      123,001 rps
INCR      111,857 rps      125,471 rps
RPUSH     126,422 rps      123,305 rps
SADD      128,041 rps      123,001 rps
HSET      116,959 rps      104,822 rps
ZADD      121,951 rps      113,250 rps

Absolute numbers vary with hardware; the point is that the memory footprint stays flat whether you run one instance or twenty, and speed is on par with Redis itself.

How it works

One tokio current-thread runtime per process (a single OS thread — hence the tiny footprint), with all command execution serialized behind one mutex, just like Redis. Each connection is an async task; pub/sub messages are pushed to subscribers over in-process channels.

Lua scripts (EVAL) run under an embedded Lua 5.1 (Redis' scripting version), holding that same keyspace mutex for the whole script — so each redis.call inside a script re-enters the dispatcher against the locked keyspace with no opportunity for another connection to interleave. Blocking commands (BZPOPMIN, XREAD BLOCK) release the mutex and park on a shared Notify that every write wakes, so idle waiters cost zero CPU.

Development

cargo test          # unit tests for the protocol, glob matching, expiry, zsets, sha1
cargo clippy        # clean

The full Redis compatibility suite lives in tests/compat/ — RESP2 fixtures diffed byte-for-byte against a real redis-server, plus a RESP3 parity check via redis-py. Run it with bash tests/compat/run.sh (needs redis-server and redis-cli on PATH; the RESP3 stage needs python3 with the redis package).

License

MIT — see LICENSE.