Any Language, One Store: How Zaris Speaks the Redis Protocol
Zaris is a distributed key/value store built natively for .NET. Its first-class clients speak an efficient binary protocol, and if your stack is .NET, that's the path you want — real async APIs, .NET types, coordination primitives, IDistributedCache and HybridCache drop-ins.
But most systems aren't only .NET. There's a Python service doing data work, a Go sidecar, a Ruby job runner, a Node gateway. Rewriting all of them onto a new client to try a new store is a non-starter. So in 2.0.0, every Zaris node can also expose a Redis-protocol (RESP2) listener: point an existing Redis client at it and use Zaris as a drop-in key/value store, in any language, without changing a line of client code.
This post is about how that front-end actually works — the routing model, the data model, the failover models, and exactly which commands are in and out.
Why put a Redis face on a .NET store
The honest reason is reach. Redis has a client in every language, and a generation of developers already knows its command vocabulary. If Zaris can accept those commands, a polyglot team gets one replicated, highly available store that every service can talk to today — while the .NET services can still drop down to the native client for typed APIs, transactions, leases, counters, and the rest.
The RESP front-end is off by default and enabled per node, because it's an opt-in compatibility surface, not the primary protocol. When it's on, a node listens on a Redis port and answers ordinary Redis commands.
The core hash is Redis-slot-aligned
Here's the part that makes it work across a cluster without a proxy in the middle. Zaris hashes keys exactly the way Redis Cluster does: CRC16(key) % 16384 gives a slot, and slots map onto Zaris's own partitions. Because the internal routing and the Redis slot math are the same, every node can advertise an accurate CLUSTER SLOTS / CLUSTER NODES view — for each slot range, the node that is the primary owner of that slot's partition, which is exactly where the key lives.
So a cluster-aware Redis client routes each key directly to its owner:
redis client ──CRC16(key) % 16384 → slot → owner──▶ node that holds the key ──▶ reply
There's no proxy and no extra hop. The client hashes the key, looks up the owner in the slot map it fetched at connect time, and connects straight to that node — the same thing it already does against real Redis Cluster. When the command lands on the node that owns the key, Zaris serves it from the in-process local store, skipping even a loopback hop. On a failover or rebalance the node answers with a MOVED, the client refreshes its slot map, and carries on. It's ordinary Redis Cluster behavior, backed by Zaris's partitioned engine.
If you'd rather not run a cluster-aware client, you don't have to — put the nodes behind a load balancer or use Sentinel instead, and any node will accept any key. Those models are below.
The whole-value model for collections
Zaris 2.0.0 has native hashes, lists, sets, and sorted sets. Over RESP, the collection commands (HSET, LPUSH, SADD, ZADD, and friends) map onto those native structures. Under the hood, a mutation is a read-modify-write over a replicated whole value: the node reads the current collection, applies your change, and writes it back with a compare-and-swap so concurrent writers can't clobber each other. That keeps the semantics correct across replication without needing per-field wire operations, and it's the same mechanism the native data-structure APIs use.
Transactions: MULTI/EXEC over real 2PC
MULTI/EXEC isn't faked. A queued transaction is executed on Zaris's native two-phase-commit transaction machinery, so the commands in the block commit together. Counter commands behave correctly inside a transaction too — INCR, DECR, INCRBY, and DECRBY read, compute, and write within the transaction rather than racing outside it. The SET key val NX PX ttl distributed-lock pattern works as well, which covers the most common Redlock-style usage.
Three ways to fail over
Because a Zaris cluster is several nodes, a Redis client needs a story for what to do when the node it's connected to goes away. Zaris supports the three failover models Redis clients already understand, so you pick the one your client library and infrastructure already assume:
Load Balancer (recommended default). Put the nodes behind a TCP load balancer and connect to the single virtual endpoint. Any node accepts any key, so the balancer can send you anywhere and a dead node simply drops out of rotation. This is the simplest model and the one we'd reach for first.
Sentinel. For clients built around Redis Sentinel, Zaris answers the Sentinel discovery commands so the client can find a node and re-resolve after a failure, using the same code path it already has.
Cluster. For clients that speak Redis Cluster, Zaris presents cluster topology with CRC16-aligned hashing — the same slot math Redis Cluster clients use — so a client's idea of which slot a key belongs to matches Zaris's own routing. (This alignment is part of why 2.0.0 is a major release; see the 2.0.0 announcement.)
What's in, and what's cleanly out
We ran the full StackExchange.Redis surface against a Zaris RESP node and classified every call. The result: ~126 commands supported, ~92 cleanly not-supported, and zero failures. "Cleanly not-supported" is the important phrase — an unsupported command returns a proper error, it doesn't crash the connection or corrupt state.
Supported covers the everyday core: strings and GET/SET/DEL, EXPIRE/TTL, MGET/MSET, INCR/DECR, hashes, lists, sets, sorted sets, Streams (XADD/XREAD/XREADGROUP with consumer groups and blocking reads), MULTI/EXEC/WATCH, SUBSCRIBE/PUBLISH, and keyspace notifications.
Cleanly rejected are the features that assume a different engine underneath: Lua scripting (EVAL), geo commands, HyperLogLog, bitmaps, cross-key *STORE operations, blocking list pops, RDB/AOF persistence commands, CLUSTER administration, and PUBSUB introspection. Treat Zaris as a fast, highly available KV + collections + streams store that speaks Redis — not as a drop-in for every Redis module. The full grid is in the Redis command compatibility matrix.
Pin your client versions
The listener is RESP2. Because the endpoint is RESP2-only and this is a compatibility surface rather than a full Redis reimplementation, pin your Redis client library versions rather than floating them — a client that silently negotiates RESP3 or reaches for an unsupported command should fail predictably, and pinning keeps that behavior stable across deploys.
Connecting
Once a node has its RESP listener enabled, it's just a Redis endpoint:
redis-cli -h node-a.internal -p 6379 SET user:42 "ada"
redis-cli -h node-a.internal -p 6379 GET user:42
import redis
r = redis.Redis(host="node-a.internal", port=6379)
r.set("user:42", "ada")
print(r.get("user:42"))
// StackExchange.Redis, unchanged
var mux = ConnectionMultiplexer.Connect("node-a.internal:6379");
var db = mux.GetDatabase();
db.StringSet("user:42", "ada");
Where to go next
The RESP front-end is how a polyglot team adopts Zaris without a rewrite; the native .NET client is how a .NET service gets the most out of it. If you're migrating an existing Redis app, Coming From Redis walks the practical path, and if you care about read throughput, Reads That Scale With Your Cores shows how direct-to-owner routing lets reads scale with the cluster.
- Enable and configure it: Redis protocol (RESP) compatibility
- Command-by-command support: compatibility matrix
- Is Zaris right for you: Clustron vs Redis · Redis alternative for .NET
- Install: clustron.io/install