Skip to content

Ask the HotKV docs

Answers come only from these docs and can be wrong; check the linked pages before you rely on them.

Migrate a cluster to HotKV

Last updated
Not on HotKV CloudOn-Premise: no license needed

HotKV uses the same 16384 slots and the same key hashing as Redis Cluster and Valkey cluster, and your cluster clients keep working unchanged, so moving a cluster means moving its data and then pointing the application at the new addresses. This page covers the ways to do it, with what each one needs from the source, and the limits you should know before you start. For a single server, see Migrate from Redis or Valkey.

HotKV and Redis or Valkey nodes cannot be in one cluster, and a Redis or Valkey replica cannot follow a HotKV primary or the other way round (see What does not work). So a migration always builds a separate HotKV cluster and copies the data into it. Plan for one cutover.

The examples use a Redis 8.10.2 cluster and a Valkey 9.1.2 cluster of three primaries each, holding example data: strings, hashes, lists, sets, sorted sets and streams, some with a time to live, and a function library. The target is a HotKV cluster. Their addresses are 127.0.0.1 with ports 7000 to 7002 (Redis), 7050 to 7052 (Valkey) and 7010 to 7025 (HotKV). The key counts and key names in the transcripts come from that example data, not from yours.

Method From Redis From Valkey Needs
Start HotKV on the shards’ files Works Does not work: HotKV does not read Valkey’s snapshot Downtime for the files, and the same slot ranges
Live copy with MIGRATE ... COPY Works Does not work: HotKV refuses the payloads of Valkey 9.1 The source nodes can reach the HotKV nodes
Live copy by value Not run, the methods above do the job Works for the types listed Read access to the source
Dual writes Works Works Your application writes to both clusters

Use the shards’ files when you can accept a short stop of writes: it is the fastest and moves everything the files hold. Use a live copy to prepare the HotKV cluster while the old one serves, then refresh it and cut over. A Valkey cluster needs the copy by value.

HotKV reads Redis snapshots (dump.rdb) up to the format of Redis 8.10: every data type HotKV has, with its TTLs, and the function libraries. A key of a type HotKV does not have, such as a module’s type or a Redis 8 vector set, makes HotKV refuse the file and stop the start (see Migrate from Redis or Valkey). For a cluster, give each HotKV node the file of one source primary, start it, and give it the slots that primary owned.

  1. Learn the slot ranges of the source:

    $ redis-cli -p 7000 CLUSTER NODES | cut -d" " -f2,3,9
    127.0.0.1:7001@17001 master 5461-10922
    127.0.0.1:7000@17000 myself,master 0-5460
    127.0.0.1:7002@17002 master 10923-16383
  2. Stop writes to the source, and save each primary. A stop with SHUTDOWN SAVE writes the file and stops the node; keep the node’s directory:

    $ redis-cli -p 7000 SHUTDOWN SAVE

    (The same for 7001 and 7002.) The file is dump.rdb in the node’s directory. Everything written after the file was made is not in it.

  3. Put each dump.rdb in the data directory of a new, empty HotKV node and start the node with cluster-enabled yes. HotKV loads the file when its directory has no HotKV snapshot yet, and leaves the Redis file as it is:

    $ for p in 7010 7011 7012; do hotkv --port $p --cluster-enabled yes --dir node-$p --daemonize yes --logfile hotkv.log; done
    $ grep -E "oad|RDB|rdb" node-7010/hotkv.log | head -n 6 | cut -c1-200
    [2026-01-15 10:00:00.334] [hotkv] [info] No snapshot of HotKV's yet (node-7010/dump.hkv), but Redis's node-7010/dump.rdb: loading it (HotKV saves to node-7010/dump.hkv and leaves it as it is)
    [2026-01-15 10:00:00.339] [hotkv] [info] Loaded the Redis snapshot node-7010/dump.rdb (written by Redis 8.10.2): 10414 keys
    [2026-01-15 10:00:00.339] [hotkv] [info] Loaded snapshot from node-7010/dump.rdb
    $ for p in 7010 7011 7012; do echo $p $(redis-cli -p $p DBSIZE); done
    7010 10414
    7011 10366
    7012 10421

    The key counts match the source, 10414, 10366 and 10421.

  4. Give each node the slot range of the primary whose file it loaded, and join the nodes. CLUSTER ADDSLOTSRANGE assigns a range, and CLUSTER MEET joins a node (an IP address, not a name):

    $ redis-cli -p 7010 CLUSTER ADDSLOTSRANGE 0 5460
    OK
    $ redis-cli -p 7011 CLUSTER ADDSLOTSRANGE 5461 10922
    OK
    $ redis-cli -p 7012 CLUSTER ADDSLOTSRANGE 10923 16383
    OK
    $ redis-cli -p 7010 CLUSTER MEET 127.0.0.1 7011; redis-cli -p 7010 CLUSTER MEET 127.0.0.1 7012
    OK
    OK
    $ redis-cli -p 7010 CLUSTER INFO | grep -E "cluster_state|cluster_slots_ok|cluster_known_nodes"
    cluster_state:ok
    cluster_slots_ok:16384
    cluster_known_nodes:3
    $ redis-cli --cluster check 127.0.0.1:7010 | grep -E "keys|OK|ERR|WARN"
    127.0.0.1:7010 (a96e1d84...) -> 10414 keys | 5461 slots | 0 slaves.
    127.0.0.1:7012 (68380f0b...) -> 10421 keys | 5461 slots | 0 slaves.
    127.0.0.1:7011 (da2b8bbe...) -> 10366 keys | 5462 slots | 0 slaves.
    [OK] 31201 keys in 3 masters.
    1.90 keys per slot on average.
    [OK] All nodes agree about slots configuration.
    [OK] All 16384 slots covered.
  5. Check your data, then add replicas as for any new cluster (see Scaling a cluster):

    $ redis-cli -c -p 7010 GET user:12345; redis-cli -c -p 7010 HGETALL profile:7; redis-cli -c -p 7010 TTL session:7; redis-cli -c -p 7010 XLEN events:3
    value-12345
    name
    n7
    visits
    7
    3527
    1

The slot ranges must match: each node holds the keys of its source primary, and a key that is in a slot the node does not own is not served from there. Take the ranges from CLUSTER NODES as shown, whatever shape the source has. If your source has replicas, start with the primaries’ files only and add the replicas afterwards.

A live copy fills the HotKV cluster while the old cluster keeps serving. The script below walks each source primary, and for each key sends MIGRATE ... COPY REPLACE to the HotKV node that owns the key’s slot, so the key stays on the source and keeps its remaining time to live. It needs pip install redis (in a virtual environment, see Cluster clients), and every source node must reach every HotKV node over the network. Set TARGET_PASSWORD when the HotKV nodes have a password.

"""Copy every key of a Redis or Valkey cluster into a HotKV cluster, routing each key by slot.
Usage: python3 copy_keys.py SOURCE_HOST:PORT TARGET_HOST:PORT
Set TARGET_PASSWORD when the HotKV nodes need a password.
"""
import os
import sys
from redis.cluster import RedisCluster
src_host, src_port = sys.argv[1].rsplit(":", 1)
dst_host, dst_port = sys.argv[2].rsplit(":", 1)
password = os.environ.get("TARGET_PASSWORD")
src = RedisCluster(host=src_host, port=int(src_port))
dst = RedisCluster(host=dst_host, port=int(dst_port), password=password)
auth = ["AUTH", password] if password else []
def owner(key):
node = dst.nodes_manager.slots_cache[dst.keyslot(key)][0]
return node.host, node.port
copied, failed, first_error = 0, 0, None
def run(pipe):
global copied, failed, first_error
for result in pipe.execute(raise_on_error=False):
if isinstance(result, Exception):
failed += 1
first_error = first_error or str(result)
else:
copied += 1
for primary in src.get_primaries():
conn = src.get_redis_connection(primary)
pipe = conn.pipeline(transaction=False)
for key in conn.scan_iter(count=1000):
host, port = owner(key)
# COPY keeps the key on the source, REPLACE overwrites a key already in HotKV
pipe.execute_command("MIGRATE", host, port, key, 0, 5000, "COPY", "REPLACE", *auth)
if len(pipe) >= 500:
run(pipe)
run(pipe)
print(f"copied {copied} keys, {failed} failed" + (f" (first error: {first_error})" if failed else ""))

Run it against an empty HotKV cluster created in the usual way (see Quick start). It sends one key per MIGRATE, because a Redis or Valkey cluster refuses a MIGRATE of several keys that are in different slots with CROSSSLOT:

$ python3 copy_keys.py 127.0.0.1:7000 127.0.0.1:7020
copied 31201 keys, 0 failed
$ redis-cli --cluster check 127.0.0.1:7020 | grep -E "keys|OK"
127.0.0.1:7020 (1d8bad6d...) -> 10414 keys | 5461 slots | 1 slaves.
127.0.0.1:7022 (20c941a9...) -> 10421 keys | 5461 slots | 1 slaves.
127.0.0.1:7021 (a51ccce8...) -> 10366 keys | 5462 slots | 1 slaves.
[OK] 31201 keys in 3 masters.
1.90 keys per slot on average.
[OK] All nodes agree about slots configuration.
[OK] All 16384 slots covered.
$ redis-cli -c -p 7020 GET user:12345; redis-cli -c -p 7020 HGETALL profile:7; redis-cli -c -p 7020 TTL session:7; redis-cli -c -p 7020 XLEN events:3
value-12345
name
n7
visits
7
3461
1
$ redis-cli -p 7020 INFO keyspace; redis-cli -p 7000 INFO keyspace
# Keyspace
db0:keys=10414,expires=63,avg_ttl=3461281,subexpiry=0
# Keyspace
db0:keys=10414,expires=63,avg_ttl=3466177,subexpiry=0

The counts, the keys that have a time to live (63) and the average time to live match between the two clusters. The copy does not move changes: a key changed on the source afterwards keeps its old value in HotKV until you run the script again, which overwrites every key:

$ redis-cli -c -p 7000 SET user:12345 changed; redis-cli -c -p 7020 GET user:12345
OK
value-12345
$ python3 copy_keys.py 127.0.0.1:7000 127.0.0.1:7020
copied 31201 keys, 0 failed
$ redis-cli -c -p 7020 GET user:12345
changed

A key deleted on the source stays in HotKV, so a final copy needs the writes stopped, or dual writes, to be exact. Run the copy once to fill the target, then again just before the cutover. The script copies database 0, as a cluster node serves.

This script reads each key with the commands of its type and writes it to the HotKV cluster, so it does not depend on the dump format. It handles strings, hashes, lists, sets, sorted sets and streams, with their time to live. It does not copy consumer groups of streams, hash field expiry, or any other type: a key of another type is counted and skipped.

"""Copy the keys of a Valkey cluster into a HotKV cluster by value.
Handles strings, hashes, lists, sets, sorted sets and streams (without consumer
groups), with their time to live. Usage: python3 copy_values.py SOURCE_HOST:PORT TARGET_HOST:PORT
Set TARGET_PASSWORD when the HotKV nodes need a password.
"""
import os
import sys
from redis.cluster import RedisCluster
src_host, src_port = sys.argv[1].rsplit(":", 1)
dst_host, dst_port = sys.argv[2].rsplit(":", 1)
src = RedisCluster(host=src_host, port=int(src_port))
dst = RedisCluster(host=dst_host, port=int(dst_port), password=os.environ.get("TARGET_PASSWORD"))
copied, skipped = 0, {}
for primary in src.get_primaries():
conn = src.get_redis_connection(primary)
for key in conn.scan_iter(count=1000):
kind = conn.type(key).decode()
ttl = conn.pttl(key)
if kind == "string":
value = conn.get(key)
dst.set(key, value)
elif kind == "hash":
dst.delete(key)
dst.hset(key, mapping=conn.hgetall(key))
elif kind == "list":
dst.delete(key)
dst.rpush(key, *conn.lrange(key, 0, -1))
elif kind == "set":
dst.delete(key)
dst.sadd(key, *conn.smembers(key))
elif kind == "zset":
dst.delete(key)
dst.zadd(key, dict(conn.zrange(key, 0, -1, withscores=True)))
elif kind == "stream":
dst.delete(key)
for entry_id, fields in conn.xrange(key):
dst.xadd(key, fields, id=entry_id)
else:
skipped[kind] = skipped.get(kind, 0) + 1
continue
if ttl > 0:
dst.pexpire(key, ttl)
copied += 1
print(f"copied {copied} keys, skipped {skipped}")
$ python3 copy_values.py 127.0.0.1:7050 127.0.0.1:7080
copied 31201 keys, skipped {}
$ redis-cli --cluster check 127.0.0.1:7080 | grep -E "keys|OK"
127.0.0.1:7080 (84c395fa...) -> 10414 keys | 5461 slots | 1 slaves.
127.0.0.1:7081 (cb5efb5c...) -> 10366 keys | 5462 slots | 1 slaves.
127.0.0.1:7082 (4764841c...) -> 10421 keys | 5461 slots | 1 slaves.
[OK] 31201 keys in 3 masters.
...
$ redis-cli -c -p 7080 XRANGE events:3 - +
1791066501452-0
k
v
$ redis-cli -c -p 7050 XRANGE events:3 - +
1791066501452-0
k
v

Every key arrived, with its stream IDs. Like the previous script, it overwrites and does not delete, and one run is a snapshot of the moment it passed each key.

To keep the two clusters in step while you copy and test, have the application write every change to both clusters. The old cluster stays the source of truth, so a write that fails on HotKV is not an outage and is fixed by the next copy. This script stands in for the application: it writes numbered keys to both.

"""Write to the old cluster and the new HotKV cluster, so that both hold every new write.
Usage: python3 dualwrite.py OLD_HOST:PORT NEW_HOST:PORT SECONDS
"""
import sys
import time
from redis.cluster import RedisCluster
old_host, old_port = sys.argv[1].rsplit(":", 1)
new_host, new_port = sys.argv[2].rsplit(":", 1)
old = RedisCluster(host=old_host, port=int(old_port), decode_responses=True)
new = RedisCluster(host=new_host, port=int(new_port), decode_responses=True)
n, start = 0, time.time()
while time.time() - start < float(sys.argv[3]):
key = f"order:{n}"
old.set(key, n) # the old cluster stays the source of truth until the cutover
new.set(key, n) # a failed write here is logged and fixed by the next copy
n += 1
time.sleep(0.005)
print(f"wrote {n} keys to both clusters")

Run it for 20 seconds while the copy script runs against the same clusters. After both have finished, compare the clusters: they should hold the same keys, and no string key should differ:

$ python3 dualwrite.py 127.0.0.1:7000 127.0.0.1:7020 20 &
$ python3 copy_keys.py 127.0.0.1:7000 127.0.0.1:7020
copied <n> keys, 0 failed
$ python3 cmp.py
keys: old=<n> new=<n>; string keys that differ: 0

The comparison script (cmp.py) counts the keys of both clusters and compares every string key:

from redis.cluster import RedisCluster
old = RedisCluster(host="127.0.0.1", port=7000, decode_responses=True)
new = RedisCluster(host="127.0.0.1", port=7020, decode_responses=True)
n = sum(old.get_redis_connection(p).dbsize() for p in old.get_primaries())
m = sum(new.get_redis_connection(p).dbsize() for p in new.get_primaries())
diff = 0
for key in old.scan_iter(count=1000):
if old.type(key) == "string" and old.get(key) != new.get(key):
diff += 1
print(f"keys: old={n} new={m}; string keys that differ: {diff}")

The steps of a cutover with this method:

  1. Build the HotKV cluster, with replicas, the same maxmemory and ACL users as the old one (see below), and test it with your application.
  2. Start dual writes in the application.
  3. Run the copy (by file, MIGRATE or by value), and compare the two clusters.
  4. Read from HotKV for a part of the traffic, and compare answers.
  5. Switch the reads to HotKV, then stop writing to the old cluster. Keep it for a while as the way back.

A HotKV node cannot join a Redis or Valkey cluster, nor the other way round. CLUSTER MEET between the two answers OK, but the handshake never completes: the other node stays handshake and disconnected, and the clusters do not mix:

127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7020
OK
$ redis-cli -p 7000 CLUSTER NODES | cut -d" " -f2,3,8 | sort
127.0.0.1:7000@17000 myself,master connected
127.0.0.1:7001@17001 master connected
127.0.0.1:7002@17002 master connected
127.0.0.1:7020@17020 handshake disconnected
127.0.0.1:7020> CLUSTER MEET 127.0.0.1 7001
OK
$ redis-cli -p 7020 CLUSTER NODES | cut -d" " -f2,3,8 | grep 7001
127.0.0.1:7001@17001 master,handshake disconnected

A Redis replica cannot follow HotKV, and a HotKV replica cannot follow Redis. So you cannot make HotKV a replica of the old cluster to catch up and switch over:

127.0.0.1:7040> REPLICAOF 127.0.0.1 7042
OK
$ redis-cli -p 7040 INFO replication | grep -E "^(role|master_host|master_link_status|master_sync_in_progress)"
role:slave
master_host:127.0.0.1
master_link_status:down
master_sync_in_progress:0
$ grep -o "Master does not support PSYNC or is in error state (reply: -ERR PSYNC here serves HotKV replicas only" redis.log | tail -n 1
Master does not support PSYNC or is in error state (reply: -ERR PSYNC here serves HotKV replicas only
127.0.0.1:7041> REPLICAOF 127.0.0.1 7043
OK
$ redis-cli -p 7041 INFO replication | grep -E "^(role|master_host|master_link_status|master_sync_in_progress)"
role:slave
master_host:127.0.0.1
master_link_status:down
master_sync_in_progress:0
$ grep -o "cannot connect to primary [0-9.:]*: it refused" node-7041/hotkv.log | tail -n 1
cannot connect to primary 127.0.0.1:7043: it refused

(Node 7040 is Redis and follows HotKV on 7042; 7041 is HotKV and follows Redis on 7043.)

Anything else that asks HotKV to act as the primary of a Redis replica, such as redis-cli --rdb or --replica, gets the same refusal of PSYNC.

What stays the same and what does not carry over

Section titled “What stays the same and what does not carry over”

The slot of a key is the same: both use CRC16 of the key, or of the hash tag in braces. The slots of these keys, asked from HotKV on port 7010 (a Redis node and a Valkey node returned the same number for each of these ten keys), including the cases with empty or repeated braces:

$ for k in user:1 user:99999 {cart1}:a profile:7 abc "{a}x" "{}x" "a{}b" "k{a}{b}" 日本語; do b=$(redis-cli -p 7010 CLUSTER KEYSLOT "$k"); echo "$k $b"; done
user:1 10778
user:99999 11051
{cart1}:a 3311
profile:7 1098
abc 7638
{a}x 15495
{}x 10595
a{}b 13694
k{a}{b} 15495
日本語 12788

What the data move does not carry, and what to do:

Item What happens
ACL users Not in a snapshot or a copy. They are per node: recreate them with ACL SETUSER on every HotKV node, or give every node the same ACL file.
Functions Loaded with the snapshot files. The live copies do not copy them: load them with FUNCTION LOAD on each node.
Cached scripts, client connections, pub/sub subscriptions Not part of the data. Applications reconnect and reload scripts.
Modules HotKV loads none. A key that holds a module’s type cannot be loaded or copied (see Migrate from Redis or Valkey).
Configuration Carries only as you copy it: HotKV reads redis.conf syntax, and directives it does not use are logged and ignored.
Reported version INFO reports redis_version:8.4.0 (the setting compat-redis-version), which is the version your clients see.

HotKV nodes need their own cluster-enabled yes, maxmemory and persistence settings. Size maxmemory by the data you copied: a node that is over the limit while a copy runs answers OOM to the copy.