valkey-bun began with one of those small local annoyances that is easy to mistake for weather. A development stack wants Redis semantics. The app expects a cache, a session store, a counter, a little pub/sub, perhaps a stream or two. Nobody is claiming this is the central nervous system of civilization. It is only there so a local service boots in a shape that resembles production closely enough for the application to tell the truth.
Somehow the usual answer is still to start another container, wait for another daemon, add another health check, explain another port collision to yourself in the tone of a parent who has just stepped on a toy in the dark, and then pretend this was all part of the feature work.
valkey-bun is an argument against that little ceremony: a Valkey/Redis-compatible server written in TypeScript for the Bun runtime, built for the narrow and therefore honest case where a local stack wants Redis protocol and Redis-shaped behavior, but not a production Valkey server. A TypeScript process is not a durable datastore. A local process still satisfies enough of the contract for the container to stay asleep.
README confidence is cheap. Redis clients are less polite.
First came the client conversation: ioredis opening a socket, node-redis negotiating RESP3, PING returning PONG, a session hash surviving a TTL check, 200 concurrent INCR calls landing on 200, and a WATCH transaction moving a balance from Alice to Bob without pretending local memory had become a bank.
After that, “Redis-compatible” stopped being a flattering adjective and became a set of conversations the server had to survive.
Checkout has to answer
In the checkout I inspected, bun test tests/ ran 45 tests across three files in 658 ms: 45 pass, 0 fail, 128 expectations. Those numbers are not heroic. They are exactly the right scale for the claim.
One file checks broad ioredis behavior: strings, hashes, lists, sets, sorted sets, transactions, pub/sub, server commands, streams. Another checks node-redis, including RESP3, because compatibility that only works with one friendly client is not yet compatibility. The sample-app file is the more revealing one. It asks for the things a local app actually does: a session store with TTL and pipelined reads, a page-view counter under contention, a leaderboard, two subscribers receiving events, a WATCH transfer, SET EX plus GETEX, a little queue, and a stream log.
A fake server passes a happy-path SET/GET demo and still betrays the first real application. Client libraries probe. They ask for INFO, CLIENT, COMMAND, CONFIG, response modes, data shapes, and error behavior. They make assumptions nobody writes in the quick-start.
Those tests make the hidden negotiation visible. node-redis connects with RESP: 3; ioredis subscribes and publishes; a hash returns the shape the session code expects; 200 increments do not collapse into a pleasant lie; MULTI and EXEC preserve reply order; WATCH lets the transfer see whether the watched keys changed.
None of that proves durability. It proves the narrower thing, which is the thing the project claims.
Small on purpose
README has the right warning. There is no RDB. There is no AOF. There is no replication, Sentinel, or cluster sharding. If the value makes users angry, auditors interested, or lawyers suddenly attentive, this is not the place to store it. Use the real server, configured by people who enjoy disaster recovery more than is strictly healthy.
Local development is full of state that deserves no such ceremony. Session hashes. Page-view counters. Cache entries. Small queues. Pub/sub events. Test data whose highest ambition is to vanish cleanly after the test process exits. Too many stacks treat all of these as if they deserved the same operational furniture as production. Disposable counters become tiny museums of systems nobody meant to operate today.
valkey-bun takes the opposite bet. Bun’s TCP server owns the socket path. RESP2 and RESP3 are parsed directly. HELLO 3 exists because clients ask for it. Code is divided by the concerns one expects: src/server.ts owns the listen loop and per-connection dispatch, src/resp.ts parses and writes the wire protocol, src/store.ts keeps the in-memory data structures, and src/commands/ groups the command handlers by Redis family.
Un-glamorous organization is one of its virtues. A local infrastructure replacement earns trust by being inspectable. If the server exists to remove ceremony from development, replacing that ceremony with a mysterious box of runtime magic and a tutorial whose first step is spiritual surrender betrays the whole bargain.
Protocol before API
For local infrastructure, compatibility often beats completeness. A test app ignores most commands Redis learned during its long and eventful life. It cannot ignore the commands common clients probe for, the response modes they negotiate, the errors they expect, and the data-shape assumptions buried inside their libraries.
Command coverage is deliberately broad: strings, keys, hashes, lists, sets, sorted sets, streams, pub/sub, transactions, connection commands, and server/configuration commands. The receipts are ordinary: HSET and HGETALL for a session, EXPIRE and TTL for time, ZADD and ZREVRANGE for a leaderboard, SUBSCRIBE and PUBLISH across two subscribers, XADD and XRANGE for a small event log, MULTI and EXEC when several writes have to travel together.
Some commands are real implementations. Others are compatibility stubs because clients ask questions that a small local server answers politely without becoming an operations product. That distinction is the boundary. A polite answer to INFO helps. Pretending to be a replicated cluster would be dangerous.
Completeness is the wrong ambition. Common local paths have to feel boring: SET and GET, TTLs, HSET session blobs, INCR under contention, ZADD leaderboards, SUBSCRIBE and PUBLISH, MULTI and EXEC, WATCH for optimistic concurrency, stream entries for little event logs. These are the places where application code leans on Redis as a shape, not as a managed service.
Binary safety is where the protocol stops being an API sketch. Redis keys and values are binary-safe, while JavaScript very much wants to turn everything into a string with a reassuring smile. valkey-bun stores values as Uint8Array where possible, and maps arbitrary key bytes through a latin1 bijection so JavaScript’s string-only Map keys do not quietly launder the data. Small enough to miss. Large enough to corrupt the promise.
Unromantic details make compatibility real.
Forgetting is a feature
Expiry is handled in the way a small in-memory service can afford: lazy checks on reads, plus a periodic sweep. Nobody designs a database for heroic uptime that way, but it is entirely respectable for a workspace process. A key expires. A test observes the TTL. A session behaves like a session. Then the whole server disappears at the end of the run, taking its little pile of temporary facts with it.
Ephemerality is not a shameful limitation here. It draws the boundary.
Software teams often talk about local development as if the only respectable local service is a smaller, worse replica of production. Sometimes that is true. If the system depends on persistence semantics, replication lag, failover behavior, cluster slots, operational metrics, or the exact strange behavior of a hosted provider under load, a toy replacement is not a development convenience; it is a lie with a friendly port number.
Many local services are not asking for a replica. They are asking for a contract: here is a Redis URL; store this value, read it back, increment it, expire it, publish a message, subscribe to a channel, and run a small transaction. A deliberately modest process satisfies that contract when everyone remembers where the boundary is.
Lightweight local substitutes are not the problem. Trouble starts when the substitute forgets to announce what it is substituting. valkey-bun is quite plain about the line. No persistence. No replication. No cluster. No blocking command semantics in the places where parking the connection would turn the project into a different beast. Enough compatibility for local development, tests, demos, and internal tools. Not enough ceremony to require its own operator.
Smaller stack, clearer contract
Bun gives the project its local texture. It already wants to be the fast runtime, the tool one reaches for when starting and testing ought to feel nearly immediate. Putting a Redis-compatible TCP service in the same ecosystem has the satisfying quality of removing one more adapter from the bench. bun install, bun run start, connect a normal Redis client, and the process listens on 0.0.0.0:6379 unless told otherwise.
Configuration follows the same plain mood: VALKEY_HOST, VALKEY_PORT, VALKEY_PASSWORD, VALKEY_DBCOUNT, VALKEY_MAXCLIENTS, and an optional cluster-size setting that forks multiple Bun workers with reusePort. Compatibility commands report those values because clients and tooling often ask about the room before deciding whether to sit down.
None of this is grand. Good local infrastructure often feels slightly anticlimactic. It removes a wait, deletes a container, shortens a test setup, and gives the developer one fewer process to think about while preserving enough of the real contract that application code does not drift into fantasy.
Small replacements are safest when their smallness is explicit. valkey-bun is not trying to become Valkey. It is a workspace instrument for the parts of Valkey that local software actually touches. Speak the protocol. Store the bytes. Keep the common shapes working. Forget on purpose.
A local service is allowed to know exactly when its memory ends.