Two ways to build Valkey out of several nodes: Cluster and Sentinel
The difference is not fault tolerance — both have it. Four things separate them at once: how the data is laid out, who decides on a failover, who answers the client's question of where to send the command, and what the client itself has to be able to do. Plus a fifth dimension people forget most often: the version. The rule "a cluster has exactly one database" is true for Redis and for Valkey before 9.0, and false for Valkey 9 — and it is re-measured here on two clusters differing by a single config line.
Full technical treatment
TL;DR
- These are two answers to one question: where should the client send the command. In a native cluster the data node answers; under Sentinel a separate service does.
- A cluster shards, Sentinel does not. Key → one of 16,384 slots → node. Under Sentinel all the data is on one primary; replicas exist only to replace it.
- Sharding changes the set of commands:
MSET foo 1 bar 2givesCROSSSLOT, because the keys landed in different slots. - On databases the received wisdom is out of date. "A cluster has
one database" is the rule of Redis and of Valkey before 9.0. In Valkey 9 a
cluster does have databases;
cluster-databasessets how many, and it defaults to 1. - Under failure the client sees different things. In a cluster, a meaningful answer and the new address from any live node. Under Sentinel, silence from the old address.
- The most dangerous place in Sentinel mode: a returning old primary
answered
OKto writes for ten seconds, and then they vanished. The client saw no error.
Who knows where to send the command
The whole difference follows from here. In a cluster every data node knows the map: come to the wrong one and it will not refuse, it will name the right one. Under Sentinel a data node knows nothing about the cluster topology — it knows only its own replication pair — and a separate service has to be asked.
cluster node: SET foo 1 -> MOVED 12182 127.0.0.1:48711
node under sentinel: CLUSTER INFO -> ERR This instance has cluster support disabled
sentinel: SENTINEL get-master-addr-by-name cache -> 127.0.0.1:41979
Key, slot, node
Addressing takes two steps. The first is arithmetic:
HASH_SLOT = CRC16(key) mod 16384. The second is cluster state: the slot
belongs to someone. That is why moving data renames no key — it changes a slot's
owner.
Braces in a key name are not decoration: only what is inside them is hashed.
{u}:a and {u}:b land in one slot and end up on one node.
What breaks in a cluster
| command | in a cluster | under Sentinel |
|---|---|---|
MSET foo 1 bar 2 | CROSSSLOT Keys in request don't hash to the same slot | OK |
MSET {u}:a 1 {u}:b 2 | works: shared brace | OK |
SWAPDB 0 1 | ERR SWAPDB is not allowed in cluster mode | OK |
The refusal is not about "two writes at once" but about two different slots: a command runs entirely on one node. The set of keys you will ever touch with one command is decided before the first write.
Numbered databases: here the version decides
The claim "a cluster has exactly one database" is easiest to check with
SELECT 1 → ERR DB index is out of range. The answer is real, the conclusion
is not: that is the rule of Redis and of Valkey before 9.0.
cluster A: cluster-databases 1 SELECT 1 -> ERR DB index is out of range
cluster B: cluster-databases 16 SELECT 1 -> OK
Two clusters, one line of difference. The database number takes no part in addressing: the same key holds different values in db 0 and db 1, but its slot is the same one — 12182.
What databases in a cluster do NOT give you: SWAPDB stays forbidden, and
DBSIZE, SCAN and FLUSHDB see only part of a database — the part on the
node the command reached. Three keys in one database spread across three nodes,
and each node counted one.
A primary fails
Comparing durations is pointless — they are configurable. Compare what the client received.
in the cluster, polling a live node:
t= 0.00 MOVED 12182 127.0.0.1:36355 (the slot's old address)
t= 3.39 CLUSTERDOWN The cluster is down (the slot is covered by nobody)
t= 3.49 MOVED 12182 127.0.0.1:39129 (the new address)
under sentinel, polling the old address:
t= 0.00 Could not connect to Valkey at 127.0.0.1:44393
t= 2.30 the sentinel names 127.0.0.1:41979 as the primary
In the cluster the client kept getting meaningful answers from a live node. Under Sentinel nobody told it the new address — it has to ask.
The most dangerous place
The old primary came back — and came back as a primary: that is what it remembers being.
t= 0.06 role master, write -> OK
t=10.10 role slave, write -> READONLY You can't write against a read only replica.
right after demotion: GET who old
after the resync: GET who the key is gone
For ten seconds writes were accepted, then erased by the resync with the real
primary. The client saw no error. In this experiment the cluster had no such
window: the returned node answers MOVED to the new address immediately.
Hence the rule in the Sentinel client spec: re-resolve the address on every reconnection, not once at startup.
Which one to pick
one node is not enough memory or throughput → cluster
fault tolerance only, data fits one node → Sentinel
code leans on MSET over arbitrary keys → Sentinel is simpler
you need numbered databases → Valkey 9+: either one
you need SWAPDB → Sentinel only
the client cannot speak the cluster protocol → no cluster for you
the client cannot speak sentinel → no Sentinel for you
The last two lines matter: both modes require their own support in the
client, and the two abilities are independent. An ordinary client that knows
nothing of clusters will, on receiving MOVED, simply hand the error text to
the application — it keeps no slot map.
What measured this
Everything was taken on Valkey 9.1.2 built from source, on one host. The
scripts and recorded runs are in bench/valkey-cluster/.
The seconds carry nowhere: a node counts as down after one second of silence instead of the default thirty. The verbatim server replies carry over completely — they are behaviour, not measurement.
And one thing carries worst of all: the version. Numbered databases in a cluster arrived in 9.0; atomic slot migration in Valkey 9.0 and in Redis 8.4. Rules about clusters taken from knowledge of Redis have to be tagged with a version.
TL;DR
"Valkey clustering" means two different things, and the choice between them is usually made on fault tolerance — which both have. The choice should be made on something else: who answers the client's question of where to send the command.
- A native cluster answers with an address. A key maps to one of 16,384
slots, a slot belongs to a node, and a node whose slot it is not returns not
data but
MOVED 12182 127.0.0.1:48711. Any node will do: every one of them knows the whole map. - Sentinel is not a cluster but a watcher. The data sits on an ordinary
primary with replicas; there is no sharding at all. A data node answers
CLUSTER INFOwithERR This instance has cluster support disabled. The primary's address is known to a separate service, and the client must ask it. - Sharding changes the set of usable commands. In a cluster
MSET foo 1 bar 2isCROSSSLOT Keys in request don't hash to the same slot: a command runs entirely on one node, and the keys landed in different slots. Under Sentinel there is one node, and the same command works. - On numbered databases the received wisdom is out of date. "A
cluster has exactly one database" is the rule of Redis and of Valkey before
9.0. In Valkey 9 a cluster does have databases; their number is set by
cluster-databases, and it defaults to one — which is why the old rule still looks true. Re-measured on two clusters differing by a single config line. - And different behaviour under failure. In a cluster the client gets a meaningful answer and the slot's new address from any live node. Under Sentinel the old address simply goes silent, and nobody will tell the client the new one.
- The most dangerous place in Sentinel mode is the old primary coming back.
Measured: for ten seconds it comes up as a primary and answers
OKto writes, and once demoted to a replica the resync erases them. The client sees no error at all. - Found along the way: "the cluster is up" and "the cluster survives a
failure" are different states. 6.07 seconds apart, and a primary that
dies inside that window is never replaced — at the default value of
cluster-replica-validity-factor. - Quorum and majority are different numbers. Five sentinels, quorum 2, two
alive: quorum satisfied, no failover, and
-failover-abort-not-electedin the log.SENTINEL CKQUORUManswers that question in advance. - The version is a fifth dimension of the comparison, and the one forgotten most often. Numbered databases in a cluster arrived in Valkey 9.0; atomic slot migration in Valkey 9.0 and in Redis 8.4. Any rule inherited from knowledge of Redis Cluster has to be tagged with a version — or checked.
Everything was taken on Valkey 9.1.2 built from source; the whole runs are in
bench/valkey-cluster/runs/.
One question, answered by two different parties
Nearly every comparison of Cluster and Sentinel starts from a table reading
"sharding: yes / sharding: no". That is true, but it is a consequence rather
than a cause, and none of the practical differences follow from it — not why
MSET stops working, not why the client needs a special library, not where the
window comes from in which writes go nowhere.
Four things separate the two modes at once, and conflating them is expensive:
DATA LAYOUT a cluster shards across 16,384 slots; Sentinel does not,
all the data sits on one primary
WHO DECIDES cluster — the nodes themselves, by a majority of primaries;
Sentinel — a separate service, by a majority of watchers
WHO ADDRESSES cluster — the data node, with an ordinary command;
Sentinel — that same service, with a command of its own
WHAT THE CLIENT each mode needs its own support in the library,
CAN DO and those two abilities are independent
The first line gives rise to the set of usable commands, the second to the behaviour under failure, the third to the connection protocol, the fourth to whether you can take the mode at all with your library. They are taken in turn below, and the third is the place to start: it explains fastest why the two modes get confused in the first place.
So: the client has a question — where do I send this command. The two modes answer it from different sides.
| native cluster | Sentinel | |
|---|---|---|
| who is asked | any data node | a separate service |
| what comes back | "not my slot, go there: 127.0.0.1:48711" | "the primary is 127.0.0.1:41979" |
| what the data node does | names the right address itself | runs the command, knows nothing of the topology |
| where knowledge of the topology lives | inside the data | beside the data, in another service |
The rest of the article is consequences of that fork, and each of them is checked by a run rather than argued. Arguing them would not work anyway: the data layout and the version add conditions of their own, and the single question "who answers with an address" does not explain every difference.
How a key becomes an address
Cluster addressing takes two steps, and it is worth holding both in mind because it is precisely these two that get conflated. The first step, from key to slot, is pure arithmetic:
HASH_SLOT = CRC16(key) mod 16384
The second step, from slot to node, is not arithmetic but cluster state: slots belong to someone, and that ownership changes. Hence a property that is otherwise hard to explain: moving data between nodes renames no key. The slot changes owner; the formula stays the same.
This is checked with one command sent to each of six nodes in turn. The client
has no -c flag — it is deliberately "dumb" and follows nothing, so that what
the server actually said is visible:
| node | answer to SET foo 1 |
|---|---|
| 44997 (primary) | MOVED 12182 127.0.0.1:48711 |
| 53953 (primary) | MOVED 12182 127.0.0.1:48711 |
| 48711 (primary) | OK |
| 39529 (replica) | MOVED 12182 127.0.0.1:48711 |
| 54583 (replica) | MOVED 12182 127.0.0.1:48711 |
| 51665 (replica) | MOVED 12182 127.0.0.1:48711 |
MOVED is a protocol error — a redirection error: the command was not
executed, and the node returned not data but the address of whoever will
execute it. It does not usually become an application failure, though: a cluster
client repairs its slot map on such an answer and retries by address. Note that
primaries and replicas answered alike: all of them know the map.
The difference between "not executed and redirected" and "not executed, outcome
unknown" matters here, and it should be kept apart from a retry after a network
failure. On MOVED a retry is safe: the node definitely did nothing. If the
connection broke after the command was sent, the client does not know whether it
ran — and there the retry is a question of idempotence, not of routing.
What a real client actually does
The block above shows the protocol but not the behaviour: a real library hides
MOVED. What it does is counted by the nodes themselves — from their own
processed-command counters, with a control window of equal length and no client
(cluster nodes talk to each other constantly, and without a control the client's
traffic cannot be told from the background).
The client was given one address out of six, and not the one holding the key:
| node | idle window | with the client | what arrived in the second window |
|---|---|---|---|
| 51665 (the address given) | 2 | 7 | `client |
One trip for the slot map (cluster|slots) — and the write went straight to the
right node. Hence what people usually mean by "the cluster sorts itself out":
the client need not know every node, one live address is enough, and there is no
middleman between the client and the data.
Under Sentinel the order is reversed, and the same measurement shows it: first the question to the service, then the command to the data. The data node took no part in finding itself, and could not have:
| asked of | answer |
|---|---|
data node 43471: CLUSTER INFO | ERR This instance has cluster support disabled |
| data node 43471: role | master |
sentinel: SENTINEL get-master-addr-by-name cache | 127.0.0.1:41979 |
One formulation should be narrowed right here, because it suggests itself in a form wider than what was measured. "A data node knows nothing about the topology" is wrong: it knows its own replication pair perfectly well.
| asked of the node under Sentinel | answer |
|---|---|
primary: INFO replication → role | master |
| primary: connected replicas | connected_slaves:2, slave0:…port=45047,state=online, slave1:…port=52369,state=online |
replica: INFO replication → role | slave |
| replica: its primary and the channel | master_host:127.0.0.1, master_port:34351, master_link_status:up |
primary: CLUSTER INFO | ERR This instance has cluster support disabled |
primary: SENTINEL get-master-addr-by-name cache | ERR unknown command 'sentinel' |
What it does not know is the authoritative answer to "who is this service's primary right now". It has an opinion, and during a failover that opinion is sometimes stale; that is exactly what happens in the section on the returning address.
The Sentinel client spec describes the required sequence in plain words — and it has three steps, not one:
it should attempt a connection with the primary, and call the ROLE command in
order to verify the role of the instance is actually a primary.
The third step — verifying with ROLE — looks like belt and braces right up to
the section about the returning address. There it becomes clear what it guards
against.
And what it does not give: it is a check, not a proof. If both the service's
answer and the node's own opinion are stale — and that is precisely the pair you
get in the first seconds after a failover — ROLE will answer master, and the
client will take the old address for the right one. The spec calls this case
rare and relies on the sentinels tearing such connections down later; it
promises no consistency.
What breaks in a cluster
Out of this grows the list of commands that stop working. The list is not arbitrary: a command runs entirely on one node, so a command needing two keys from different slots cannot run anywhere.
| slot | |
|---|---|
CLUSTER KEYSLOT foo | 12182 |
CLUSTER KEYSLOT bar | 5061 |
CLUSTER KEYSLOT {u}:a | 11826 |
CLUSTER KEYSLOT {u}:b | 11826 |
| command | in a cluster | under Sentinel |
|---|---|---|
MSET foo 1 bar 2 | CROSSSLOT Keys in request don't hash to the same slot | OK |
MSET {u}:a 1 {u}:b 2 | MOVED 11826 127.0.0.1:42549 — that is, it runs | OK |
The refusal is not about "two writes at once" but about two different slots: the same two writes with a shared brace go through. Braces are not decoration in a name:
If the key contains a "{...}" pattern only the substring between `{` and `}` is hashed in order to obtain the hash slot.The practical consequence is costliest in hindsight: in a cluster the set of keys you will ever need to touch with one command is decided before the first write. Under Sentinel that decision need not be made at all.
The second item on the list is numbered databases — and this is where the article got it wrong. The mistake is instructive, so it is dealt with not by swapping a line but in a section of its own.
Numbered databases: here the answer depends on the Valkey version
The claim "a cluster has exactly one database" is easiest to check with one experiment:
| command | in a cluster | under Sentinel |
|---|---|---|
SELECT 1 | ERR DB index is out of range | OK |
SWAPDB 0 1 | ERR SWAPDB is not allowed in cluster mode | OK |
— and drew from it the conclusion "a cluster has exactly one database". The server's answer is real, the conclusion is not. The only correct conclusion from that single experiment is: THIS cluster is configured with one database. Telling the two readings apart needed a second experiment, and there wasn't one.
Because in Valkey 9.0 numbered databases in a cluster appeared:
Valkey 9.0 adds the ability to have numbered databases on a cluster, changing
everything about that advice.
The number of databases is set by a separate option, cluster-databases, and it
defaults to one — which is why a cluster brought up the ordinary way behaves the
old way. Two clusters differing by exactly that line:
| cluster A | cluster B | |
|---|---|---|
cluster-databases | 1 | 16 |
SELECT 0 | OK | OK |
SELECT 1 | ERR DB index is out of range | OK |
SELECT 15 | ERR DB index is out of range | OK |
SELECT 16 | ERR DB index is out of range | ERR DB index is out of range |
One config line — and SELECT 1 turns from a refusal into OK. The option is
immutable (IMMUTABLE_CONFIG in src/config.c), so it is chosen when the node
starts and cannot be changed on the fly.
The database number takes no part in addressing
That is the second question, and it is the more interesting one: does the new feature break the two-step addressing the whole cluster rests on. It does not.
| answer | |
|---|---|
node owning the slot of key foo | 127.0.0.1:51799 |
in db 0: SET foo db0 | OK |
in db 1: SET foo db1 | OK |
in db 0: GET foo | db0 |
in db 1: GET foo | db1 |
in db 0: CLUSTER KEYSLOT foo | 12182 |
in db 1: CLUSTER KEYSLOT foo | 12182 |
One key, two values, one slot. The database is added as a third dimension inside the node, and the layout of data across nodes does not depend on it by a single key.
And what it does NOT give you
From "databases exist now" it is just as easy to build the opposite myth — that in a cluster they behave as they do on a single node. They do not:
| answer | |
|---|---|
SWAPDB 0 1 | ERR SWAPDB is not allowed in cluster mode |
| three keys written into db 1 | foo → slot 12182, bar → slot 5061, user:1 → slot 10778 |
DBSIZE in db 1 on node 37191 | 1 |
DBSIZE in db 1 on node 50991 | 1 |
DBSIZE in db 1 on node 51799 | 1 |
SCAN in db 1 on the owner node | 0 foo |
SCAN in db 1 on node 37191 | 0 bar |
SWAPDB was forbidden and stayed forbidden. And DBSIZE and SCAN count not
the database as a whole but its part on the node the command reached: three
keys were written into one database, spread across three nodes, and no node sees
all three. The same goes for FLUSHDB. The documentation says so directly:
will return the number of keys in the current database on the connected node
From the same page, on what numbered databases should not be taken for:
numbered databases do not provide any form of resource isolation
The upshot: a numbered database in a cluster is a namespace, not a separate store. Splitting environments across them is fine; expecting isolation, or a cluster-wide view of their contents, is not.
Hence the rule worth taking away from the article as a whole
The error here is not carelessness. It is that the knowledge about clusters came from where everyone takes it: shared memory of Redis Cluster, where there really is one database. On Valkey 9 that rule stopped being true — and still looks true, because the default value confirms it.
| capability | Valkey ≤ 8.x | Valkey 9.0 | Valkey 9.1 |
|---|---|---|---|
| db 0 in a cluster | yes | yes | yes |
| several dbs in a cluster | no | yes | yes |
cluster-databases | no | yes, default 1 | yes, default 1 |
SELECT n in a cluster | only 0 | up to cluster-databases − 1 | up to cluster-databases − 1 |
SWAPDB in a cluster | no | no | no |
The rule is simple: any statement about clusters inherited from knowledge of Redis has to be tagged with a version — or checked. Checking it here cost one config line and two minutes.
A primary fails: one event, two client experiences
Here is the main practical difference, and it is not in the seconds. Comparing durations is pointless: both are configurable, and in the benchmark both were deliberately shortened so the run would fit in minutes. What should be compared is the text the client receives.
In the cluster a live node was polled — not the one that died:
| t, s | what the client received |
|---|---|
| 0.00 | node 36355 killed |
| 0.00 | MOVED 12182 127.0.0.1:36355 |
| 3.39 | CLUSTERDOWN The cluster is down |
| 3.49 | MOVED 12182 127.0.0.1:39129 |
| — | the returned node: role slave, SET foo → MOVED 12182 127.0.0.1:39129 |
Three different answers, all three meaningful: the slot's old address, an honest "the slot is covered by nobody right now", the new address. The client never had to guess — the node it reached knows the state of the whole cluster, because nodes exchange it continuously over the bus. And no external service was consulted: the replica was promoted by the nodes themselves, by a majority of primaries.
Under Sentinel the same event looks different:
| t, s | what happened |
|---|---|
| 0.00 | node 44393 killed |
| 0.00 | old address: Could not connect to Valkey at 127.0.0.1:44393 |
| 2.30 | the sentinel names 127.0.0.1:41979 as the primary |
| — | the new primary answers a write with OK |
| — | the other replica: READONLY You can't write against a read only replica. |
The old address answered nothing — there is nobody there. No data node named the new address to the client and none could: the nodes do not know about their pair's service identity, and the service that does know does not call. Hence the rule in the client spec:
every time a reconnection is needed, the client should resolve again the
address using Sentinels restarting from Step 1.
Not "preferably", but every time. Why, is the next section.
The old primary comes back
This is the most dangerous place in Sentinel mode, and it is about recovery rather than failure. The killed primary is brought back — and comes back as a primary: that is what it remembers being, and nobody has told it otherwise yet.
| t, s | role and answer |
|---|---|
| 0.06 | role master, write → OK |
| 10.10 | role slave, write → READONLY You can't write against a read only replica. |
| right after demotion | GET who → old |
| after the resync | GET who → the key is gone |
For ten seconds the node accepted writes and answered OK. The last two rows
read together and in that order: right after demotion the write is still there,
and after the resync the key is gone — the data was replaced with a copy of the
real primary. The client got OK, and the write vanished, without seeing a
single error.
In this experiment no such window appeared in the cluster, and that is checked by the same run — the last two rows of the cluster table above. The returned node learned from the bus that its slots belong to another before it took a single command.
"In this experiment" is not politeness. One scenario was checked — killed and brought back up; on start the node read the bus and got a fresh configuration before the client's first command. A network partition is a separate case in the cluster specification, and there a bounded loss of writes is admitted: a client with a stale map writes to a former primary that has not yet learned of its demotion. We did not measure that, and the article will not claim the cluster has no such window at all. What was measured is something else: in a cluster that window closes by itself, and under Sentinel only by the client's own actions.
This is what the ROLE check in the client spec guards against. A client that
holds a data address directly and cannot re-ask the sentinel falls into that
window whole.
Found along the way: ready to work ≠ ready to fail
This section came out of a bug in the benchmark. The script brought up a
cluster, waited for cluster_state:ok on every node and killed a primary. The
replica was never promoted — not in twenty seconds, not in a minute, never.
It turned out not to be a bug in the benchmark:
| signal of readiness | t, s |
|---|---|
cluster_state:ok on all nodes (slots covered, requests served) | 0.01 |
master_link_status:up on all replicas (someone can be promoted) | 6.08 |
| the window between them | 6.07 |
Inside those six seconds the cluster looks entirely healthy: slots are covered, keys are written and read. But the replicas have not completed a single sync — and a primary that dies here is never replaced:
Currently unable to failover: Disconnected from primary for longer than
allowed. Please check the 'cluster-replica-validity-factor' configuration
option.
Why "never" rather than "until some timeout expires" is visible in the source:
if (server.repl_state == REPL_STATE_CONNECTED) {
data_age = (mstime_t)(server.unixtime - server.primary->last_interaction) * 1000;
} else {
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}For a replica that has never connected to its primary, repl_down_since is
zero. Its data age comes out measured from the Unix epoch, and no allowance
covers an age like that.
That "never" needs narrowing, and it is narrowed by measurement rather than by a
disclaimer. The freshness check on a replica's data is governed by
cluster-replica-validity-factor; in the experiment above it holds its default.
The same experiment with zero:
cluster-replica-validity-factor | 0 |
cluster-node-timeout | 2000 ms |
| t=0.00 | primary 34455 killed, waiting up to 40 s |
| t=3.53 | cluster state fail, replica role slave |
| t=4.15 | cluster state fail, replica role master |
| replica promoted | yes |
One config line — and "never" turns into four seconds to promotion. So the correct formulation is: at the default value, a replica that has completed no sync is never promoted; an option turns that off. No advice to do so follows: the check is there for a reason, and a replica that has never synced is promoted empty — a failover of that kind cures availability at the cost of data.
The conclusion is not about Valkey but about operations: "the cluster is up"
and "the cluster survives a failure" are different checks. The first answers
whether requests are served; the second requires looking at
master_link_status on every replica. Automation that rolls out a cluster and
immediately tears down the old one passes the first and fails the second.
Quorum and majority are different numbers
In sentinel monitor <name> <ip> <port> <quorum> the last number is read as
"how many sentinels suffice to fail over". That is wrong, and the documentation
says so directly:
The quorum is only used to detect the failure. In order to actually perform a
failover, one of the Sentinels need to be elected leader... and be authorized to
proceed. This only happens with the vote of the majority of the Sentinel
processes.
The numbers were chosen so that one condition holds and the other does not: five sentinels, quorum 2, three killed. The quorum is satisfied — and there is no failover:
| sentinels in the set | 5 |
| quorum in the configuration | 2 |
| majority of five | 3 (not configurable anywhere) |
| sentinels alive | 2 |
| failover within 30 s | none |
+sdown master cache 127.0.0.1 48147
+odown master cache 127.0.0.1 48147 #quorum 2/2
+new-epoch 1
+try-failover master cache 127.0.0.1 48147
+vote-for-leader c92cd70b21a9dd26ec40e67185371e78ee502f9a 1
-failover-abort-not-elected master cache 127.0.0.1 48147
#quorum 2/2 — the configured condition is met and the primary is declared
down. And right after it -failover-abort-not-elected: two votes out of five
were not enough to become leader. Bringing back one of the killed sentinels gave
a majority, and the failover completed in two seconds — at the very same quorum.
None of this has to be discovered during the incident. One command answers in advance, and it checks both conditions:
| sentinels alive | SENTINEL CKQUORUM cache |
|---|---|
| 5 | OK 5 usable Sentinels. Quorum and failover authorization can be reached |
| 2 | NOQUORUM 2 usable Sentinels. Not enough available Sentinels to reach the majority and authorize a failover |
The refusal names the second condition in so many words: there are not enough watchers to reach a majority and authorize a failover — while the configured quorum is met. That makes it a ready-made monitoring signal: it answers "will we fail over if the primary dies right now", not "is everything fine at this moment".
A practical rule that does not follow from one setting: the system will not survive losing a majority of its sentinels, however low the quorum. Hence the minimum in the documentation:
You need at least three Sentinel instances for a robust deployment.
How this differs from Redis
A fair question: Valkey inherited both mechanisms from Redis whole, and up to a point this is literally the same code. The boundary is named inside Valkey itself, in the same file as the version number:
#define VALKEY_VERSION "9.1.2"
/* Redis OSS compatibility version, should never
* exceed 7.2.x. */
#define REDIS_VERSION "7.2.4"So Valkey declares compatibility up to 7.2.4 — the version at which the projects parted.
That line cannot be used as proof, though, and it is worth saying so plainly. It means exactly one thing: a shared past up to 7.2.4. It does not follow that everything before that point still matches today, nor that everything after it diverged. Both sides have since changed things that used to be common. The only honest way to compare is to take a capability and read the current documentation of both projects; that is what is done below, and the result was not what the reasoning "it arrived in Valkey, so Redis has not got it" leads to.
The first example is atomic slot migration, introduced in Valkey 9.0.
Valkey 9.0 introduced a option for migrating hash slots known as atomic slot
migration, which is faster, more reliable, and has less impact on client
applications than the legacy CLUSTER SETSLOT-based migration.
What matters here for the reader: the old way of moving slots is the very one
that keeps ASK redirection alive in the specification, along with the warning
that multi-key operations may answer TRYAGAIN while a slot is on the move.
Atomic migration shortens that phase.
But the conclusion "and Redis has nothing of the kind", which suggests itself here, is wrong. Redis Open Source 8.4 gained atomic migration too, under a command of its own:
This command allows you to import slots from other nodes, monitor the progress
of migration tasks, and cancel ongoing migrations... Executes on the
destination master. Accepts multiple slot ranges and triggers atomic migration
for the specified ranges.
So as of today both have the capability, while the commands and the protocol
differ: in Valkey it is CLUSTER MIGRATESLOTS plus the internal
CLUSTER SYNCSLOTS, in Redis CLUSTER MIGRATION. Operationally that is a more
practical difference than "present or absent": migration procedures and the
tooling around them do not carry between the projects, even where the capability
goes by the same name.
| capability | Valkey | Redis |
|---|---|---|
| shared past up to | 7.2.4 | 7.2.4 |
| several dbs in a cluster | since 9.0, cluster-databases | no |
| atomic slot migration | since 9.0, CLUSTER MIGRATESLOTS | since 8.4, CLUSTER MIGRATION |
Everything else in this article — slots, MOVED, CROSSSLOT, the shape of
Sentinel, quorum against majority — came to both projects from a shared past.
None of it was re-checked here on Redis, and the article will not claim
"Redis does exactly the same": not measured, not stated. That is precisely what
undoes that kind of claim.
Which one to pick
The analysis is short, and it is not about throughput.
you need more memory or throughput than one node gives
→ native cluster: it is the only one of the two that shards
the data fits one node and you need fault tolerance only
→ Sentinel stays a candidate
the code leans on multi-key commands over arbitrary keys
→ Sentinel is simpler; in a cluster the keys must be pulled
into one slot with braces
you need numbered databases
→ Valkey before 9: Sentinel only
→ Valkey 9+: a cluster too, via cluster-databases
you need SWAPDB
→ Sentinel only: in a cluster it is forbidden in 9.x as well
does the client speak the cluster protocol?
→ no: a cluster is out of reach without replacing the client or a proxy
does the client speak sentinel?
→ no: Sentinel is out of reach without replacing the client or a proxy
The last two lines deserve a careful reading: the received "the client can't
speak sentinel → take a cluster" leads up to the idea that a cluster forgives a
naive client. It does not. An ordinary client that knows
nothing of clusters, on receiving MOVED, will simply hand the error text to
the application: it keeps no slot map, cannot retry by address, does not know
ASK and ASKING, does not handle TRYAGAIN. Both modes require their own
support in the client, and the two abilities are independent: a library may
have one, the other, both or neither. With neither, nothing works, and the
question moves from choosing a mode to choosing a library or a proxy.
What is true and stayed true: a cluster client needs one live address to get the map. In production several are listed anyway — in case that particular node is unreachable at startup.
And one more caveat about multi-key commands that was missing from the first
version. The CROSSSLOT refusal is a server contract. Some client libraries
offer a convenient multi-key call on top of it, splitting the request into
several commands by node. That works as a wrapper, not as the same command: it
has no atomicity, its failure model is different (some sub-requests may succeed
and others not), and the latency is the sum of several round trips.
What should reproduce, and what should not
The numbers were taken on Valkey 9.1.2 (built from source — the README in
bench/valkey-cluster/ says how), on one host, processes on random ports. The
scripts and the recorded runs open from here: bench/valkey-cluster/.
The seconds carry nowhere. A node counts as down after one second of silence
instead of the default thirty, and cluster-node-timeout is 2000 ms instead of
15,000. That was done so the set would fit in minutes. The ten seconds of the
recovery block and the 6.07 seconds of the readiness block will differ on your
settings — but that both windows exist does not depend on settings.
The verbatim server replies carry over completely. MOVED, CROSSSLOT,
ERR DB index is out of range, READONLY, -failover-abort-not-elected are
behaviour rather than measurement, and on the same version they will be the same.
Across versions, not necessarily. Everything was taken on 9.1.2; these runs were repeated neither on 8.x nor on Redis. And the numbered-databases section above is the article's own demonstration of why that caveat is not boilerplate.
Corrections
12 September 2026Collected here is what this material got wrong, and what settled it. Every entry rests on a run or on a document rather than on our having changed our minds.
"A cluster has exactly one database" — and from it the conclusion that numbered databases do not exist in a cluster at all.
In Valkey 9 a cluster has databases. Their count is set by cluster-databases, and its default is 1 — which is exactly why the old rule still looks right. It has not stopped being the rule of Redis and of Valkey before 9.0; it was never a universal law.
Two fresh clusters differing by one line of config: with cluster-databases 1 the command SELECT 1 answers ERR DB index is out of range, with cluster-databases 16 it answers OK. The database number takes no part in addressing: one and the same key holds different values in databases 0 and 1 while its slot stays 12182 (bench/valkey-cluster/databases.py).
"Atomic slot migration arrived in Valkey 9.0 — so Redis has nothing of the kind."
Both have the capability; the protocols differ. In Redis Open Source 8.4 atomic migration arrived under its own command, CLUSTER MIGRATION instead of CLUSTER MIGRATESLOTS.
The current documentation of both projects, quoted in the text. This is a correction of the METHOD of comparison: that a capability is announced in one project implies nothing about the other — comparison must go capability by capability against current documentation rather than by dates of arrival. Nothing here was re-measured on Redis, and the article does not claim otherwise.
"A data node under Sentinel knows nothing about the topology."
A node knows its own replication pair perfectly well: a primary sees its replicas, a replica sees its primary. What it does not know is the membership of the whole group and who decides on a failover.
INFO replication run on both sides of the pair (bench/valkey-cluster/boundaries.py). The wording is narrowed to what was measured.
"The client can't speak sentinel → take a cluster" — and from it the idea that a cluster forgives a naive client.
It does not. An ordinary client that knows nothing of clusters, on receiving MOVED, hands the error text to the application: it keeps no slot map, cannot retry by address, does not know ASK and ASKING, does not handle TRYAGAIN. Both modes require their own client support, and the two abilities are independent.
This is a reading of the contract rather than a measurement: the behaviour of MOVED, ASK and TRYAGAIN is set out in the cluster specification quoted in the text.
The CROSSSLOT refusal was presented with no caveat about client libraries.
The CROSSSLOT refusal is the SERVER's contract. Some client libraries build a convenient multi-key call on top of it by splitting it into several commands across nodes. That is a wrapper, not the same command: it has no atomicity, its failure model differs, and its latency is the sum of several round trips.
An added caveat rather than a changed claim: the refusal itself was measured and is unchanged, but without this frame a reader carries the server's contract over to the behaviour of their own library.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- "Valkey clustering" means two different things, and the choice between them is usually made on fault tolerance — which both have. The choice should be made on something else: who answers the client's question of where to send the command.
- A native cluster answers with an address. A key maps to one of 16,384 slots, a slot belongs to a node, and a node whose slot it is not returns not data but
MOVED 12182 127.0.0.1:48711. Any node will do: every one of them knows the whole map. - Sentinel is not a cluster but a watcher. The data sits on an ordinary primary with replicas; there is no sharding at all. A data node answers
CLUSTER INFOwithERR This instance has cluster support disabled. The primary's address is known to a separate service, and the client must ask it. - Sharding changes the set of usable commands. In a cluster
MSET foo 1 bar 2isCROSSSLOT Keys in request don't hash to the same slot: a command runs entirely on one node, and the keys landed in different slots. Under Sentinel there is one node, and the same command works. - On numbered databases the received wisdom is out of date. "A cluster has exactly one database" is the rule of Redis and of Valkey before 9.0. In Valkey 9 a cluster does have databases; their number is set by
cluster-databases, and it defaults to one — which is why the old rule still looks true. Re-measured on two clusters differing by a single config line. - And different behaviour under failure. In a cluster the client gets a meaningful answer and the slot's new address from any live node. Under Sentinel the old address simply goes silent, and nobody will tell the client the new one.
- The most dangerous place in Sentinel mode is the old primary coming back. Measured: for ten seconds it comes up as a primary and answers
OKto writes, and once demoted to a replica the resync erases them. The client sees no error at all. - Found along the way: "the cluster is up" and "the cluster survives a failure" are different states. 6.07 seconds apart, and a primary that dies inside that window is never replaced — at the default value of
cluster-replica-validity-factor. - Quorum and majority are different numbers. Five sentinels, quorum 2, two alive: quorum satisfied, no failover, and
-failover-abort-not-electedin the log.SENTINEL CKQUORUManswers that question in advance. - The version is a fifth dimension of the comparison, and the one forgotten most often. Numbered databases in a cluster arrived in Valkey 9.0; atomic slot migration in Valkey 9.0 and in Redis 8.4. Any rule inherited from knowledge of Redis Cluster has to be tagged with a version — or checked.
- Everything was taken on Valkey 9.1.2 built from source; the whole runs are in
bench/valkey-cluster/runs/.
In fact
- Sentinel is not a cluster at all: it shards nothing. All the data sits on one primary, and replicas exist only so that there is someone to replace it. One command checks this: a node under sentinel answers
CLUSTER INFOwithERR This instance has cluster support disabled. Hence the thing these two modes are usually confused over: if the data does not fit one node, Sentinel does not help at all — it solves a different problem, fault tolerance, and solves it without sharding. - That is the rule of Redis and of Valkey before 9.0, and in Valkey 9 it is wrong. Numbered databases in a cluster arrived in 9.0, and a separate option,
cluster-databases, sets how many there are; it defaults to one — which is why a cluster brought up the ordinary way refusesSELECT 1and the old rule keeps looking true. Re-measured on two clusters differing by that one line: with 16, SELECT 0 through 15 all work. The database number takes no part in addressing — the same key holds different values in db 0 and db 1 at one and the same slot, 12182. The opposite myth is wrong too:SWAPDBstays forbidden, andDBSIZE,SCANandFLUSHDBsee only the part of a database that lies on the node the command reached. - It is a redirection error: the command was NOT executed, but instead of data the node returned the address of whoever will execute it. The node did not refuse: it named the slot and its owner —
MOVED 12182 127.0.0.1:48711. A cluster client repairs its slot map on that answer and retries by address, and the retry is safe here: the node definitely did nothing. That has to be told apart from a retry after a broken connection, where the client does not know whether the command ran — and the question becomes idempotence rather than routing. Measured across all six nodes: primaries and replicas answer alike, because every node knows the map. - They do, if the keys are in one slot. The boundary runs by slots, not by key count:
MSET foo 1 bar 2givesCROSSSLOT Keys in request don't hash to the same slotbecause foo is in slot 12182 and bar in 5061. The same two writes asMSET {u}:a 1 {u}:b 2go through: braces make only the substring inside them hash, and both keys land in slot 11826. The price of this is not a line of code but a decision about key naming, taken before the first write. - It says only how many are needed to DECLARE the primary down. A failover can be performed only by the sentinel elected by a majority of all known sentinels, and that number is not configurable anywhere. Measured: five sentinels, quorum 2, two alive. The quorum was satisfied — the log shows
+odown ... #quorum 2/2— and right after it-failover-abort-not-elected: two votes out of five were not enough. Bringing back one killed sentinel made the failover complete in two seconds, at the very same quorum. None of this needs to be discovered during the incident:SENTINEL CKQUORUManswers in advance and checks both conditions separately. - Ready to work, yes; ready to fail, no — and 6.07 measured seconds lie between those states. Slots are covered almost at once, while the replication link between a primary and its replica takes several more seconds, and all that time
master_link_statuson the replicas isdown. A primary that dies inside the window is NEVER replaced: for a replica that has never connected, the "when did the link go down" mark stayed zero, its data age is computed from the Unix epoch, and it sticks forever on Currently unable to failover: Disconnected from primary for longer than allowed. "Forever" here is about the default value: the same experiment withcluster-replica-validity-factor 0promotes in four seconds — at the cost of promoting an empty replica. - You can, but it will come back as a PRIMARY — that is what it remembers being — and it will accept writes until the sentinel notices and demotes it. Measured: for ten seconds
SETansweredOK. After the demotion a full resync with the real primary erased those writes, and the client saw no error at all. The danger is not the window itself but who falls into it: a client holding a data address directly. In this experiment the cluster had no such window — the returned node answeredMOVEDto the slot's new owner immediately, having read the bus before its first command. - Their shared past runs to 7.2.4 — Valkey says so itself, with
REDIS_VERSION "7.2.4"sitting next toVALKEY_VERSION "9.1.2". But that line cannot be taken as proof: it does not follow from it that everything before that point still matches today, nor that everything after it diverged. Comparison has to go capability by capability, against the current documentation of both. The example that trips up the reasoning "it arrived in Valkey, so Redis has not got it": atomic slot migration arrived in Valkey 9.0 — and in Redis Open Source 8.4 as well, only under its own command,CLUSTER MIGRATIONinstead ofCLUSTER MIGRATESLOTS. Both have the capability; the protocols differ. Nothing here was re-measured on Redis. - It will — but only a client that speaks the cluster protocol can use that. An ordinary one, on receiving
MOVED, simply hands the error text to the application: it keeps no slot map, cannot retry by address, knows nothing ofASK,ASKINGorTRYAGAIN. Both modes require their own support in the library, and the two abilities are independent: it may speak cluster, sentinel, both or neither. What is true is that a cluster client needs ONE live address to obtain the map; in production several are listed anyway, in case that particular node is unreachable at startup.
What is covered
- One question, answered by two different parties
- How a key becomes an address
- What a real client actually does
- What breaks in a cluster
- Numbered databases: here the answer depends on the Valkey version
- A primary fails: one event, two client experiences
- The old primary comes back
- Found along the way: ready to work ≠ ready to fail
- Quorum and majority are different numbers
- How this differs from Redis
- Which one to pick
- What should reproduce, and what should not
Common misconceptions
Sentinel is Valkey's "lightweight cluster"
Sentinel is not a cluster at all: it shards nothing. All the data sits on one primary, and replicas exist only so that there is someone to replace it. One command checks this: a node under sentinel answers CLUSTER INFO with ERR This instance has cluster support disabled. Hence the thing these two modes are usually confused over: if the data does not fit one node, Sentinel does not help at all — it solves a different problem, fault tolerance, and solves it without sharding.
In a Valkey cluster only database 0 is available
That is the rule of Redis and of Valkey before 9.0, and in Valkey 9 it is wrong. Numbered databases in a cluster arrived in 9.0, and a separate option, cluster-databases, sets how many there are; it defaults to one — which is why a cluster brought up the ordinary way refuses SELECT 1 and the old rule keeps looking true. Re-measured on two clusters differing by that one line: with 16, SELECT 0 through 15 all work. The database number takes no part in addressing — the same key holds different values in db 0 and db 1 at one and the same slot, 12182. The opposite myth is wrong too: SWAPDB stays forbidden, and DBSIZE, SCAN and FLUSHDB see only the part of a database that lies on the node the command reached.
MOVED is an error and the client should handle it as a failure
It is a redirection error: the command was NOT executed, but instead of data the node returned the address of whoever will execute it. The node did not refuse: it named the slot and its owner — MOVED 12182 127.0.0.1:48711. A cluster client repairs its slot map on that answer and retries by address, and the retry is safe here: the node definitely did nothing. That has to be told apart from a retry after a broken connection, where the client does not know whether the command ran — and the question becomes idempotence rather than routing. Measured across all six nodes: primaries and replicas answer alike, because every node knows the map.
Multi-key commands do not work in a cluster
They do, if the keys are in one slot. The boundary runs by slots, not by key count: MSET foo 1 bar 2 gives CROSSSLOT Keys in request don't hash to the same slot because foo is in slot 12182 and bar in 5061. The same two writes as MSET {u}:a 1 {u}:b 2 go through: braces make only the substring inside them hash, and both keys land in slot 11826. The price of this is not a line of code but a decision about key naming, taken before the first write.
The number in sentinel monitor ... <quorum> says how many watchers suffice to fail over
It says only how many are needed to DECLARE the primary down. A failover can be performed only by the sentinel elected by a majority of all known sentinels, and that number is not configurable anywhere. Measured: five sentinels, quorum 2, two alive. The quorum was satisfied — the log shows +odown ... #quorum 2/2 — and right after it -failover-abort-not-elected: two votes out of five were not enough. Bringing back one killed sentinel made the failover complete in two seconds, at the very same quorum. None of this needs to be discovered during the incident: SENTINEL CKQUORUM answers in advance and checks both conditions separately.
If cluster_state:ok, the cluster is ready
Ready to work, yes; ready to fail, no — and 6.07 measured seconds lie between those states. Slots are covered almost at once, while the replication link between a primary and its replica takes several more seconds, and all that time master_link_status on the replicas is down. A primary that dies inside the window is NEVER replaced: for a replica that has never connected, the "when did the link go down" mark stayed zero, its data age is computed from the Unix epoch, and it sticks forever on Currently unable to failover: Disconnected from primary for longer than allowed
. "Forever" here is about the default value: the same experiment with cluster-replica-validity-factor 0 promotes in four seconds — at the cost of promoting an empty replica.
After a failover you can simply bring the old primary back up
You can, but it will come back as a PRIMARY — that is what it remembers being — and it will accept writes until the sentinel notices and demotes it. Measured: for ten seconds SET answered OK. After the demotion a full resync with the real primary erased those writes, and the client saw no error at all. The danger is not the window itself but who falls into it: a client holding a data address directly. In this experiment the cluster had no such window — the returned node answered MOVED to the slot's new owner immediately, having read the bus before its first command.
Valkey clustering and Redis clustering are the same thing
Their shared past runs to 7.2.4 — Valkey says so itself, with REDIS_VERSION "7.2.4" sitting next to VALKEY_VERSION "9.1.2". But that line cannot be taken as proof: it does not follow from it that everything before that point still matches today, nor that everything after it diverged. Comparison has to go capability by capability, against the current documentation of both. The example that trips up the reasoning "it arrived in Valkey, so Redis has not got it": atomic slot migration arrived in Valkey 9.0 — and in Redis Open Source 8.4 as well, only under its own command, CLUSTER MIGRATION instead of CLUSTER MIGRATESLOTS. Both have the capability; the protocols differ. Nothing here was re-measured on Redis.
A cluster forgives a naive client: any node will tell it where to go
It will — but only a client that speaks the cluster protocol can use that. An ordinary one, on receiving MOVED, simply hands the error text to the application: it keeps no slot map, cannot retry by address, knows nothing of ASK, ASKING or TRYAGAIN. Both modes require their own support in the library, and the two abilities are independent: it may speak cluster, sentinel, both or neither. What is true is that a cluster client needs ONE live address to obtain the map; in production several are listed anyway, in case that particular node is unreachable at startup.
Check yourself
A client sent SET foo 1 to a cluster node that does not own that slot. What does it get?
Sources & further reading
8 SOURCES
- Valkey — Cluster specificationOfficial documentation. The addressing rule everything else follows from: "HASH_SLOT = CRC16(key) mod 16384". And the way around it: "If the key contains a '{...}' pattern only the substring between the braces is hashed in order to obtain the hash slot." From the same page, an honest caveat about multi-key operations: "Multi-key operations may become unavailable when a resharding of the hash slot the keys belong to is in progress."https://valkey.io/topics/cluster-spec/
- Valkey — High availability with Valkey SentinelOfficial documentation. The four duties of the service, named as one list: "Monitoring, Notification, Automatic failover, Configuration provider." The last is the answer to why a client needs Sentinel at all: clients ask it "for the address of the current Valkey primary responsible for a given service." And the distinction this article checks by measurement: "The quorum is only used to detect the failure. In order to actually perform a failover, one of the Sentinels need to be elected leader... and be authorized to proceed. This only happens with the vote of the majority of the Sentinel processes." Also the minimum: "You need at least three Sentinel instances for a robust deployment."https://valkey.io/topics/sentinel/
- Valkey — Sentinel client specOfficial documentation. What a client is obliged to do, and why "just connect to the address" is wrong in this mode. Step by step: "The client should iterate the list of Sentinel addresses", then SENTINEL get-master-addr-by-name, then "it should attempt a connection with the primary, and call the ROLE command in order to verify the role of the instance is actually a primary." And the rule that explains the block about the returning address: "every time a reconnection is needed, the client should resolve again the address using Sentinels restarting from Step 1."https://valkey.io/topics/sentinel-clients/
- Valkey — Numbered Databases in Valkey 9.0Official documentation. The feature that forced a whole section to be rewritten: "Valkey 9.0 adds the ability to have numbered databases on a cluster, changing everything about that advice." From the same page, the boundaries without which one gets the opposite myth: DBSIZE "will return the number of keys in the current database *on the connected node*", the same for SCAN and FLUSHDB, and plainly: "numbered databases do not provide any form of resource isolation."https://valkey.io/blog/numbered-databases/
- Redis — CLUSTER MIGRATIONOfficial documentation. The source that corrected the section on differences. Atomic slot migration exists in Redis too: "This command allows you to import slots from other nodes, monitor the progress of migration tasks, and cancel ongoing migrations", and on the same page "Executes on the destination master. Accepts multiple slot ranges and triggers atomic migration for the specified ranges." It arrived in Redis Open Source 8.4.0 — so both projects have the capability, and the commands differ.https://redis.io/docs/latest/commands/cluster-migration/
- Valkey — Atomic slot migrationOfficial documentation. What arrived in Valkey 9.0: "Valkey 9.0 introduced a option for migrating hash slots known as atomic slot migration, which is faster, more reliable, and has less impact on client applications than the legacy CLUSTER SETSLOT-based migration." It is tempting to draw from it the conclusion "and Redis has nothing of the kind" — which is wrong: Redis Open Source 8.4 gained atomic migration too, under its own CLUSTER MIGRATION command.https://valkey.io/topics/atomic-slot-migration/
- valkey/src/cluster_legacy.c — why a replica refuses to be promotedSource code. The condition behind the finding in blocks 11-12. A replica's data age is computed as: "if (server.repl_state == REPL_STATE_CONNECTED) { data_age = (mstime_t)(server.unixtime - server.primary->last_interaction) * 1000; } else { data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000; }". For a replica that has never connected to its primary, repl_down_since is zero — so the age comes out measured from the Unix epoch, and no allowance covers it. Hence the permanently stuck "Currently unable to failover: Disconnected from primary for longer than allowed."https://github.com/valkey-io/valkey/blob/9.1.2/src/cluster_legacy.c
- valkey/src/version.h — what Valkey states about compatibilitySource code. Two lines side by side, worth knowing before any talk of differences: "#define VALKEY_VERSION \"9.1.2\"" and "/* Redis OSS compatibility version, should never exceed 7.2.x. */ #define REDIS_VERSION \"7.2.4\"". Valkey declares itself compatible up to 7.2.4 — the version at which the projects parted — and nothing guarantees that whatever each added after it still matches.https://github.com/valkey-io/valkey/blob/9.1.2/src/version.h