JusDB
Free DB Audit
NoSQL Databases

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

JusDB Team
Published November 10, 2026
18 min read

Many standalone and Sentinel-managed Valkey instances keep separate data sets in SELECT 1 to SELECT 15. Until Valkey 9.0, a cluster had only database 0, so those instances could not move. Valkey 9.0 added numbered databases in cluster mode, but the tools lag behind. First attempts usually fail in one of three ways. Clients get ERR DB index is out of range because cluster-databases is still 1. A node loading the old RDB file exits with FATAL: Data file was created with a Valkey server configured to handle more than 1 databases. Or valkey-cli --cluster import prints *** Importing N keys from DB 0, leaves databases 1 to 15 behind, and deletes the keys it moved from the source. This guide covers what 9.x allows, keeping databases versus remapping to key prefixes, moving each database, verification, and rollback.

TL;DR
  • Set cluster-databases above the highest database id in use, on every node, before first start. It defaults to 1 and cannot change at runtime.
  • valkey-cli --cluster import moves database 0 only, and it deletes source keys unless you pass --cluster-copy. Script the other databases, or load the RDB file into one empty node.
  • Run 9.0.7 or 9.1.3 or later before you use atomic slot migration with more than one database. Earlier 9.x releases could apply writes to the wrong database on the target (#4952).
  • Check every client: GLIDE 2.1 and later can select a database in cluster mode; valkey-py's ValkeyCluster rejects db.
  • Verify per database by summing INFO keyspace across primaries. valkey-cli --cluster check counts database 0 only.

Scope and review date: November 2026. This post covers moving a standalone or Sentinel-managed Valkey 7.2, 8.x or 9.x instance (or Redis OSS 7.2 and earlier) into a self-managed Valkey 9.0.7 or 9.1.3 cluster. Commands, defaults and error strings were checked against the Valkey documentation and the 9.1.3 source code on 10 November 2026. They were not run in a lab, so rehearse them on staging first.


What do numbered databases in Valkey 9 cluster mode allow and restrict?

The cluster specification says Valkey Cluster has supported multiple databases since 9.0, "with some additional restrictions", but does not list them. The list below comes from the original change, PR #1671, and the 9.1.3 source:

  • cluster-databases defaults to 1 and is immutable in config.c. The standalone databases setting is ignored in cluster mode.
  • The slot depends only on the key name, so the same key in databases 0 and 5 shares a slot.
  • SELECT, MOVE and COPY ... DB work. SWAPDB returns SWAPDB is not allowed in cluster mode. MOVE and COPY with a database return TRYAGAIN while their slot is migrating.
  • CLUSTER GETKEYSINSLOT, CLUSTER COUNTKEYSINSLOT and MIGRATE act on the currently selected database. FLUSHDB, SCAN and DBSIZE act on the connected node only (Valkey blog: Numbered Databases in Valkey 9.0).
  • Databases share CPU and memory: a namespace, not a resource boundary.
ini
# valkey.conf on every cluster node, before the first start
cluster-enabled yes
cluster-databases 16

Interpretation: valid ids then run from 0 to 15, so size the value to the highest id in use, not the number of databases. CONFIG SET cluster-databases fails with can't set immutable config, and SELECT 3 on a node still at the default returns ERR DB index is out of range. If your topology choice is still open, read Valkey Cluster vs Sentinel first.

How do I find which databases the source actually uses?

Start with the keyspace section of INFO. It lists only databases that hold keys, one line per database.

bash
valkey-cli -h 10.0.0.5 INFO keyspace
valkey-cli -h 10.0.0.5 CONFIG GET databases

Interpretation: each dbN:keys=...,expires=... line is one database to move. The highest N sets the minimum cluster-databases value. Record the keys and expires figures now; they are your baseline for verification.

Then find which clients use which database. The SELECT documentation notes that CLIENT LIST shows a db=N field for each connection.

bash
valkey-cli -h 10.0.0.5 CLIENT LIST \
  | awk '{for (i = 1; i <= NF; i++) if ($i ~ /^(name|db|user)=/) printf "%s ", $i; print ""}' \
  | sort | uniq -c

Interpretation: each row counts connections per client name, database and ACL user. A database with keys but no live connections may belong to a batch job, so check cron hosts too.

Should you keep numbered databases or remap them to key prefixes?

Keeping databases is the smaller application change: the database number stays in the connection settings. Remapping to prefixes (db3: in front of every key) works on any cluster version and any cluster client, but it touches every key name in the code.

Keep numbered databases or remap to key prefixes

Switch options to compare what changes in the server, the clients and the migration

Keep numbered databases

AreaKeep numbered databases
Server versionValkey 9.0 or later in cluster mode
Server configcluster-databases above the highest id, set before first start
Application changeDatabase number in the client configuration
Client libraryMust allow a non-zero database in cluster mode
Same key name in two databasesStays two separate keys
Memory costSmall overhead per database, not per key
Copy methodMIGRATE with destination-db N, or RDB load
Moving back to an 8.x clusterNot possible without remapping first

Remap to key prefixes

AreaRemap to key prefixes
Server versionAny Valkey Cluster version
Server configDefault cluster-databases 1
Application changeEvery key name gets a prefix
Client libraryAny cluster client
Same key name in two databasesPrefix keeps them apart in database 0
Memory costPrefix bytes repeated on every key
Copy methodDUMP, then RESTORE under the new name
Moving back to an 8.x clusterPossible: data uses database 0 only

Deciding factor: client support. If one service's cluster client cannot select a database, either upgrade that client or remap that service's database to a prefix.

Client support varies. The Valkey GLIDE configuration reference documents databaseId, which in cluster mode "requires Valkey 9.0+" and persists across reconnections; the GLIDE 2.1 changelog lists it for Node, Java, Go and Python. valkey-py 6.2.0's ValkeyCluster instead raises Argument 'db' is not possible to use in cluster mode (cluster.py). The Valkey blog also warns that pooled clients can hand out a connection with another database still selected. Test each client on staging across a node restart: one that reconnects into database 0 reads and writes the wrong data without an error.

Prefixes and hash tags

A prefix changes the key name, so it changes the slot, unless the key already has a hash tag such as {user:42}. Keep the prefix outside the braces. Never put the prefix itself inside braces ({db3}:key): that sends a whole database to one slot on one node.

Which migration method fits each database?

Choose per database, not once for the whole instance.

MethodDatabases movedWrites during copyBest for
valkey-cli --cluster importDatabase 0 onlyNot capturedDatabase 0, when nothing else is in use
RDB load into one empty cluster nodeAll, in one fileStop writes firstLarge data, an outage window is acceptable
Scripted MIGRATE ... COPYOne per run, same idNeeds a final passKeeping databases, moving online in stages
Scripted DUMP and RESTOREOne per run, any target nameNeeds a final passRemapping to prefixes, or targets the source cannot reach
Re-ingest or warm from the system of recordAnyApplication handles itCaches that can start cold

Per-database migration into a Valkey 9 cluster

Each source database lands in the same database number on the cluster, then gets its own count check

Per-database migration into a Valkey 9 cluster Step 1: the standalone source with databases 0 to 15 is read one database at a time. Step 2: each database is copied by RDB load, MIGRATE with COPY, or DUMP and RESTORE into the same database number on a Valkey 9.1 cluster with cluster-databases 16. Step 3: per-database counts on the cluster are compared with the source before clients move, and the source is retired after cutover. Standalone source db0 to db15 Copy per database MIGRATE or RESTORE Valkey 9.1 cluster cluster-databases 16 1 one database at a time 2 same db number, TTL kept 3 compare keys and expires per database, then move clients

Order: move and verify the smallest database first to prove ACLs, routing and client settings.

What does valkey-cli --cluster import actually move?

The cluster tutorial says the command "moves all the keys of a running instance". The valkey-cli 9.1.3 source is narrower: it scans the source without a SELECT, logs *** Importing %d keys from DB 0, and sends MIGRATE with destination database 0 hard-coded. The unstable branch was unchanged as of 7 November 2026.

bash
valkey-cli --cluster import 10.0.1.11:6379 \
  --cluster-from 10.0.0.5:6379 \
  --cluster-from-user migrator --cluster-from-pass "$SRC_PASS" \
  --cluster-copy --cluster-replace

Interpretation: this copies database 0 and keeps the source keys. Without --cluster-copy, each key is deleted from the source once the target accepts it, which removes your rollback copy. The source must not be a cluster node, and the command moves one key per MIGRATE call, so it is slow on large databases.

Databases 1 and up are left behind without an error

The command exits successfully after database 0. The other databases simply stay on the source, which is why verification compares every database.

Can I load the standalone RDB file straight into a cluster node?

Yes, with care. In 9.1.3, the loader exits with the FATAL ... more than N databases message if the file uses a database id at or above cluster-databases. After loading, a primary checks every slot holding keys in any database. It takes ownership of unassigned slots (I have keys for unassigned slot N. Taking responsibility for it.), but deletes keys in slots owned by another node (cluster_legacy.c). So load the file into one empty node that owns no slots, then spread the slots out.

bash
# On the source, with application writes stopped
valkey-cli -h 10.0.0.5 BGSAVE
valkey-cli -h 10.0.0.5 LASTSAVE

# New node: cluster-enabled yes, cluster-databases 16, appendonly no,
# no nodes.conf yet, the copied dump.rdb in its dir
valkey-server /etc/valkey/node-a.conf
valkey-cli --cluster fix 10.0.1.11:6379

Interpretation: when LASTSAVE changes, the snapshot is complete. The new node claims every slot that has keys, and --cluster fix covers the empty ones after asking for confirmation. Valkey reads RDB files from Redis OSS 7.2 and earlier and from Valkey, but not from Redis CE 7.4 and later (Valkey migration guide).

bash
valkey-cli -h 10.0.1.11 CONFIG SET appendonly yes
valkey-cli --cluster add-node 10.0.1.12:6379 10.0.1.11:6379
valkey-cli --cluster add-node 10.0.1.13:6379 10.0.1.11:6379
valkey-cli --cluster rebalance 10.0.1.11:6379 \
  --cluster-use-empty-primaries --cluster-use-atomic-slot-migration

Interpretation: enabling AOF after the load avoids starting from an empty append-only file. rebalance then moves slots to the new primaries with atomic slot migration, which carries every database. The --cluster-use-atomic-slot-migration flag exists in valkey-cli 9.1 and later, not 9.0.7, and the next section covers the patch floor it needs. Add replicas last.

How do I copy databases 1 to 15 with MIGRATE or DUMP and RESTORE?

MIGRATE "operates on the database currently selected in the connection" and takes a destination database (MIGRATE documentation). The source sends SELECT and a RESTORE with the remaining TTL, and the target must own each key's slot. This valkey-py script uses a standalone client for the source database and a ValkeyCluster client only to find slot owners.

python
# copy_db.py: copy one source database into the same database number on the cluster
import sys
from collections import defaultdict
import valkey
from valkey.cluster import ValkeyCluster

DB = int(sys.argv[1])
USER, PASS = "migrator", sys.argv[2]
src = valkey.Valkey(host="10.0.0.5", port=6379, db=DB, username=USER, password=PASS)
router = ValkeyCluster(host="10.0.1.11", port=6379, username=USER, password=PASS)

cursor = 0
while True:
    cursor, keys = src.scan(cursor=cursor, count=500)
    batches = defaultdict(list)
    for key in keys:
        node = router.get_node_from_key(key)
        batches[(node.host, node.port)].append(key)
    for (host, port), batch in batches.items():
        src.execute_command("MIGRATE", host, port, "", DB, 5000,
                            "COPY", "REPLACE", "AUTH2", USER, PASS, "KEYS", *batch)
    if cursor == 0:
        break

Interpretation: COPY keeps the source keys, and REPLACE lets you rerun the script as a catch-up pass. NOKEY is normal for keys that expired mid-scan. Target instance replied with error usually means missing cluster-databases headroom, a moved slot, or ACL access. Lower count for large keys, because MIGRATE blocks both instances during each batch.

The target user needs SELECT, RESTORE and access to the database. Database permissions (db=) arrived in 9.1, and SELECT into a forbidden database fails with NOPERM (ACL documentation).

ini
# users.acl, shipped to every cluster node (Valkey 9.1 or later)
user migrator on >CHANGE_ME ~* +@read +@write +select +ping alldbs
user sessions on >CHANGE_ME ~* +@read +@write -@dangerous +select db=3

Interpretation: the migrator can write to any database, while sessions can use only database 3, so a misconfigured client fails with NOPERM instead of writing to database 0. ACL users are per node.

To remap a database to a prefix instead, read with DUMP and write with RESTORE under the new name. The DUMP documentation says the payload "does NOT contain expire information", so read PTTL as well.

python
# remap_db.py: same src and router setup as copy_db.py
# Moves source database DB into cluster database 0 as "db{DB}:key"
for key in src.scan_iter(count=500):
    payload = src.dump(key)
    if payload is None:
        continue                    # expired or deleted since the scan
    ttl = src.pttl(key)             # -1 means no expiry
    router.restore(f"db{DB}:".encode() + key, max(ttl, 0), payload, replace=True)

Interpretation: a TTL of 0 tells RESTORE to create the key without an expiry. Because DUMP is a read-only command, this script also works while the source is under CLIENT PAUSE WRITE, which blocks MIGRATE.

What patch level do I need before resharding with several databases?

Valkey 9.1.3 (26 October 2026) and 9.0.7 (27 October 2026) "Fix atomic slot migration with cluster-databases > 1 applying writes made during the snapshot to the wrong database on the target" (9.1.3 release notes). Per PR #4952, the snapshot selects each database it copies, but the source's parent process never sees those SELECTs, so a write made during the snapshot could run in another database on the target. Both releases also fix unreplicated evictions on the import target (#4920) and are marked as security releases.

bash
valkey-cli --cluster call 10.0.1.11:6379 INFO server \
  | grep -o 'valkey_version:[0-9.]*'
valkey-cli -h 10.0.1.11 CONFIG GET cluster-databases

Interpretation: every node must report 9.0.7 or later on 9.0, or 9.1.3 or later on 9.1. At cluster-databases 1 the bug cannot occur. The fix does not repair keys already moved, so check any shard resharded under write load on an older release.

CLUSTER MIGRATESLOTS and CLUSTER CANCELSLOTMIGRATIONS need alldbs, and the primaryuser account needs +select (atomic slot migration documentation). The full runbook is in Resharding Valkey 9 with atomic slot migration.

Legacy reshard reads the database count with CONFIG GET

Without the atomic flag, valkey-cli --cluster reshard moves keys database by database, using CONFIG GET cluster-databases to find how many there are. If that call fails, for example because the ACL user lacks CONFIG, it prints "use default value 1 instead" and moves database 0 only.

How do I verify each database after the move?

On a cluster, DBSIZE and INFO keyspace report one node's share of each database, so add them up across primaries. Do not rely on valkey-cli --cluster check for this: its "keys" figure comes from DBSIZE on a connection in database 0.

bash
# Sum keys per database across all primaries
valkey-cli --cluster call 10.0.1.11:6379 INFO keyspace --cluster-only-primaries \
  | grep -o 'db[0-9]*:keys=[0-9]*' \
  | awk -F'[:=]' '{sum[$1] += $3} END {for (d in sum) print d, "keys=" sum[d]}' \
  | sort -V

Interpretation: compare each line with the source's INFO keyspace. With writes stopped, the counts should match exactly, except for keys that expired in between. A database present on the source and missing here was never copied.

INFO keyspace on the source, then summed across the cluster

Illustrative output from an 8.1 source and a 9.1 cluster: database 3 is 1,200 keys short after the first pass

ops@bastion:~
$ valkey-cli -h 10.0.0.5 INFO keyspace# Keyspacedb0:keys=412000,expires=398000,avg_ttl=3540112db1:keys=95000,expires=0,avg_ttl=0db3:keys=1200000,expires=1200000,avg_ttl=86011020$ valkey-cli --cluster call 10.0.1.11:6379 INFO keyspace \ --cluster-only-primaries | grep -o 'db[0-9]*:keys=[0-9]*' \ | awk -F'[:=]' '{s[$1]+=$3} END {for (d in s) print d, s[d]}'db0 412000db1 95000db3 1198800

Next step: rerun the copy script for database 3 with REPLACE and compare again. Lines follow the 8.1.11 format; 9.x nodes add a keys_with_volatile_items field.

Counts catch missing databases, not wrong values. Also spot-check a random sample of keys in each database: TYPE must match on both sides and PTTL on the cluster should be equal or slightly lower. With valkey-cli -c -n 3, the CLI follows redirects and selects database 3 again on each new connection.

How do I cut clients over and keep a way back?

Copy in bulk while the source serves traffic, then stop writes for a short final pass. The scripts do not remove keys deleted on the source after they were copied, so for exact parity run the whole copy inside the freeze, or finish by deleting cluster keys that no longer exist on the source.

Runbook: multi-database instance into a Valkey 9.x cluster

Eight steps; steps 5 to 7 are the write outage window

  1. Inventory databases and clients

    Record keys and expires per database, and map each database to the services that use it.

    valkey-cli -h 10.0.0.5 INFO keyspace
  2. Build the cluster with cluster-databases set

    Run 9.0.7 or 9.1.3 or later on every node, and set the value above the highest database id before first start.

    cluster-enabled yes cluster-databases 16
  3. Ship ACLs and test every client

    Give each application user its db= rule. Prove each client selects its database in cluster mode, across a node restart.

  4. Bulk copy each database

    Smallest database first. Keep source keys with COPY, and check counts after each database.

    python3 copy_db.py 1 "$PASS" python3 copy_db.py 3 "$PASS"
  5. Stop writes on the source

    Outage starts

    Stop writers in the application. CLIENT PAUSE WRITE also blocks MIGRATE, so use it only with the DUMP and RESTORE script.

  6. Run the final pass and verify

    Outage

    Copy again with REPLACE, remove keys deleted since the bulk copy, then compare the summed counts per database.

  7. Repoint clients to the cluster

    Outage ends

    Deploy the cluster client configuration with each service's database id, and watch ACL LOG for denials.

    valkey-cli -h 10.0.1.11 ACL LOG 10
  8. Keep the source frozen for the rollback window

    Leave it running without writers until the agreed window ends.

Rollback point: until the first write reaches the cluster in step 7, rollback is repointing clients to the untouched source. After that, you need a reverse copy with the DUMP and RESTORE script.

One more limit: a Valkey 8.x cluster refuses keys outside database 0 (its 8.1.11 startup check fails), so the way back is to the standalone source or a 9.x cluster. The Valkey 9.1 upgrade guide lists the other 9.x features that block a return to 8.x.

Before you retire the source, confirm its INFO keyspace still matches the step 1 baseline and CLIENT LIST shows no application writers.

Frequently asked questions

Can Valkey Cluster use SELECT and multiple databases?

Yes, from Valkey 9.0. Set cluster-databases in the config of every node before it starts, because the default is 1 and the value cannot change at runtime. SELECT, MOVE and COPY with a database work. SWAPDB is rejected in cluster mode, and FLUSHDB, SCAN and DBSIZE act only on the node you are connected to.

Does valkey-cli --cluster import copy all databases?

No. In Valkey 9.1.3 it scans database 0 on the source and writes to database 0 on the cluster, and it logs "Importing N keys from DB 0". It also deletes each source key after copying it unless you pass --cluster-copy. Move databases 1 and up with a MIGRATE or DUMP and RESTORE script, or by loading the RDB file into one empty cluster node.

Which Valkey version is safe for atomic slot migration with several databases?

Valkey 9.0.7 or 9.1.3, or a later patch, on every node. Earlier 9.x releases could apply writes made during the migration snapshot to the wrong database on the target when cluster-databases was above 1. Both releases also fix unreplicated evictions on the import target and carry security fixes.

Should I keep numbered databases or switch to key prefixes?

Keep databases if every client library can select a non-zero database in cluster mode and you do not need to move back to an 8.x cluster. Switch to prefixes if a client cannot, or if you need to stay compatible with older clusters. Prefixes cost memory on every key and change slots unless the key has a hash tag.

Key takeaways

  • Size cluster-databases to the highest database id in use and set it before first start; it cannot change at runtime.
  • --cluster import moves database 0 only and deletes source keys without --cluster-copy, so script the other databases or load the RDB into one empty node.
  • Run 9.0.7 or 9.1.3 or later before any atomic slot migration with more than one database.
  • Test every client's database selection in cluster mode, including reconnects and connection pools.
  • Verify each database by summing INFO keyspace across primaries, and keep the source untouched until the rollback window closes.

Planning a multi-database Valkey migration with JusDB

The risk in this move sits in details: a client that falls back to database 0, a tool that copies one database and reports success, or a patch level older than the multi-database fixes. Rehearse each database's path on staging.

JusDB's Valkey migration team can review your database inventory, client libraries and patch levels, plan the method for each database, and help run the cutover and rollback checks with your team.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read

What the Valkey Docs Get Wrong: 12 Gaps Operators Should Know in Valkey 9.1

Twelve places where valkey.io disagrees with Valkey 9.1.3, from failover timing and TLS expiry to default bind, MONITOR and CLIENT LIST, with how to check each one and report it.

Redis20 minNov 9, 2026
Read