How to Migrate From Redis to Valkey: Step-by-Step Guide (2026)
This guide is for engineers who run Redis, self-hosted, on Kubernetes or on ElastiCache, and want to move to Valkey without losing data or taking a long outage. By the end you will know which migration path your Redis version allows, you will have run that path (replication, RedisShake or a managed engine upgrade), verified the copy with a sampling script, and kept a way back. Commands were checked against Valkey 9.1.3, Redis 7.2 and 8.10.3, RedisShake 4.7.0, the Valkey Helm chart 0.12.1 and redis-py 8.1.0, current as of 10 November 2026. You need redis-cli or valkey-cli, shell access to the source and a maintenance window you probably will not use. If you are still deciding whether to move at all, read Redis vs Valkey in 2026 first.
Step 1: Find out which Redis you actually run
Everything downstream depends on one fact: the RDB format version your source writes. Valkey 7.x and 8.x, and Redis up to 7.2, all use RDB version 11. Redis 7.4 moved to 12 and Redis 8.10 writes 15. Valkey 9 uses 80 under a new VALKEY magic string. The Valkey migration guide is explicit that data files from Redis CE 7.4 and later are not compatible with Valkey, which rules out both copying dump.rdb and pointing REPLICAOF at the source.
# Run against every instance, including the ones nobody remembers owning.
redis-cli -h old-redis -p 6379 --user default --pass "$REDIS_PASS" INFO server \
| grep -E '^(redis_version|valkey_version|server_name|redis_mode|server_mode):'
# The first 9 bytes of the snapshot are the format magic + version.
redis-cli -h old-redis CONFIG GET dir
redis-cli -h old-redis CONFIG GET dbfilename
head -c 9 /var/lib/redis/dump.rdb; echo
On a local Redis 7.2.5 I got REDIS0011. Read the result against this matrix:
| Source | Snapshot magic | Supported path to Valkey 9.1 | Source |
|---|---|---|---|
| Redis 2.x to 7.2.x | REDIS0011 or lower |
Replication (REPLICAOF), RDB copy, or cluster node swap |
Valkey migration guide |
| Redis 7.4 to 8.4 | REDIS0012 |
Logical copy with RedisShake (listed as supported source) | RedisShake compatibility |
| Redis 8.6 to 8.10 | REDIS0013 to REDIS0015 |
RedisShake, after a dry run (not yet listed by RedisShake) | Redis rdb.h |
| ElastiCache Redis OSS 4.0.10 to 7.1 | managed | modify-replication-group --engine valkey |
AWS docs |
| Memorystore for Redis | managed | New Memorystore for Valkey instance plus a data copy | Memorystore versions |
Step 2: Check modules, commands and clients
Valkey will accept nearly all core commands, but modules are a different matter. Redis 8 bundles JSON, the Query Engine, time series, Bloom and vector sets; RedisShake lists vector sets and Redis Stack modules as unsupported for migration. Find out what you use before you copy anything.
# Loaded modules (Redis 7.x and 8.x) and which command families get traffic
redis-cli -h old-redis MODULE LIST
redis-cli -h old-redis INFO commandstats \
| grep -i -E 'cmdstat_(ft|json|ts|bf|cf|vadd|vsim)' | head -20
# Key types in a 5,000-key sample; module types show their own names (ReJSON-RL, ...)
redis-cli -h old-redis --scan --count 1000 | head -5000 \
| while read -r k; do echo "TYPE $k"; done \
| redis-cli -h old-redis | sort | uniq -c
If FT.*, JSON.* or vector set commands show up, plan to rebuild those structures on Valkey with valkey-search and valkey-json rather than copy them, and test the command differences.
Clients need less work. The protocol is unchanged, so redis-py, Jedis, Lettuce, go-redis and ioredis connect to Valkey as they are. Two things to check: Valkey's INFO reports redis_version:7.2.4 regardless of the real version (it is pinned in version.h), so any code that enables features by parsing that field will under-detect. And if you plan to adopt Valkey GLIDE, note its matrix covers Valkey 7.2 to 9.1 and Redis 6.2 to 7.2, so switch clients after the server move, not before.
Step 3: Migrate Redis 7.2 or older with REPLICAOF
This is the lowest-risk path: Valkey becomes a replica of Redis, catches up, and you promote it. Start a Valkey 9.1.3 server with the same maxmemory, eviction policy and persistence settings as the source.
# valkey.conf highlights (Valkey 9.1.3). primaryauth/primaryuser also accept
# the old names masterauth/masteruser. The official image runs as the valkey
# user (uid 999) and only chowns /data, so keep dir there.
cat > /etc/valkey/valkey.conf <<'EOF'
port 6379
dir /data
appendonly yes
maxmemory 6gb
maxmemory-policy allkeys-lru
replicaof old-redis.internal 6379
primaryuser default
primaryauth CHANGE_ME_SOURCE_PASSWORD
replica-read-only yes
EOF
docker run -d --name valkey -p 6379:6379 \
-v /etc/valkey:/usr/local/etc/valkey -v valkey-data:/data \
valkey/valkey:9.1.3 valkey-server /usr/local/etc/valkey/valkey.conf
Watch the sync. On a healthy link you should see this on the Valkey side (output from my local test, abbreviated):
valkey-cli INFO replication | grep -E 'role|master_link_status|master_sync_in_progress'
# role:slave
# master_link_status:up
# master_sync_in_progress:0
# On the source, the new replica should be online with near-zero lag
redis-cli -h old-redis INFO replication | grep -E 'connected_slaves|slave0'
# connected_slaves:1
# slave0:ip=10.0.3.21,port=6379,state=online,offset=...,lag=1
The field names still say master and slave because they are part of the protocol. Once master_link_status:up holds and the offsets match, run the checks in Step 7, then cut over: stop writes to the source (or point clients at Valkey), confirm the offsets are equal, and promote.
valkey-cli REPLICAOF NO ONE
valkey-cli CONFIG SET replica-read-only no # only matters if you left it on
valkey-cli CONFIG REWRITE # needs valkey.conf writable by uid 999
valkey-cli INFO replication | head -3
# role:master
# connected_slaves:0
For a Redis Cluster on 7.2 or older, the Valkey guide's approach is node by node: add each Valkey node as a replica with redis-cli --cluster add-node <valkey-ip>:6379 <existing-ip>:6379 --cluster-replica, run CLUSTER FAILOVER on it to make it a primary, repeat for every shard, then remove the Redis nodes with redis-cli --cluster del-node <any-node>:6379 <node-id>. Do one shard at a time and check CLUSTER NODES between each.
Step 4: Migrate Redis 7.4 or 8.x with RedisShake
From Redis 7.4 onward, Valkey refuses the replication stream, so you need a tool that parses the source's RDB and replays it as ordinary commands. RedisShake 4.7.0 (October 2026) does this with its sync_reader, which connects to the source as a replica, converts the full snapshot into commands, then keeps streaming incremental writes until you stop it.
First, do a dry run that touches nothing: parse a copy of the snapshot into a file. If RedisShake hits a type it does not understand (likely with Redis 8.10 compact hashes or module data), it fails here instead of halfway through production.
curl -LO https://github.com/tair-opensource/RedisShake/releases/download/v4.7.0/redis-shake-v4.7.0-linux-amd64.tar.gz
tar xzf redis-shake-v4.7.0-linux-amd64.tar.gz
cat > dryrun.toml <<'EOF'
[rdb_reader]
filepath = "/backup/dump.rdb"
[file_writer]
filepath = "/tmp/shake-dryrun.cmd"
type = "cmd"
EOF
./redis-shake dryrun.toml && wc -l /tmp/shake-dryrun.cmd
Then the real run. The cross-version setting is the important one: RedisShake's cross-version migration notes say that setting target_redis_proto_max_bulk_len to 0 makes it convert all values into plain commands (SET, RPUSH, HSET and so on) instead of sending RESTORE payloads, which avoids the binary encoding mismatch when the target is "older" in RDB terms.
# shake.toml for RedisShake v4.7.0: Redis 8.x source -> Valkey 9.1 target
[sync_reader]
cluster = false
address = "old-redis.internal:6379"
username = "default"
password = "CHANGE_ME_SOURCE_PASSWORD"
tls = false
sync_rdb = true
sync_aof = true # keep streaming writes until cutover
[redis_writer]
cluster = false
address = "valkey.internal:6379"
username = "default"
password = "CHANGE_ME_TARGET_PASSWORD"
tls = false
[advanced]
log_file = "shake.log"
status_port = 8080 # curl localhost:8080 for progress
rdb_restore_command_behavior = "panic" # stop if a key already exists on target
target_redis_proto_max_bulk_len = 0 # replay everything as plain commands
./redis-shake shake.toml
# In another terminal, watch progress until the incremental phase is near zero lag
curl -s localhost:8080 | head -40
When it reports the incremental (AOF) phase and the counters stop moving between checks, stop writes on the source, wait a few seconds, stop RedisShake and move clients. The source is untouched throughout, which is your rollback.
Step 5: Upgrade ElastiCache from Redis OSS to Valkey
On ElastiCache this is an in-place engine change. AWS says the endpoint DNS name stays the same, node IPs change, reads keep working throughout and writes pause only during a failover of a few seconds when starting from Redis OSS 5.0.6 or later. You need AWS CLI v2.18.2 or later.
The decision that matters is rollback. ElastiCache only supports rolling back from Valkey 7.2 to Redis OSS 7.1, so go in two hops: Valkey 7.2 first, soak it, then upgrade to 9.1.
# Hop 1: Redis OSS -> Valkey 7.2 (rollback to Redis OSS 7.1 is still possible).
# Pass a Valkey parameter group only if the source uses a custom one; it must
# carry the same static parameter values.
aws elasticache modify-replication-group \
--replication-group-id my-cache \
--engine valkey \
--engine-version 7.2 \
--cache-parameter-group-name my-valkey72-params \
--apply-immediately
# Hop 2, after a soak period: Valkey 7.2 -> 9.1 (no way back to Redis after this)
aws elasticache modify-replication-group \
--replication-group-id my-cache \
--engine-version 9.1 \
--cache-parameter-group-name my-valkey9-params \
--apply-immediately
# Serverless caches use the major version only
aws elasticache modify-serverless-cache \
--serverless-cache-name my-serverless-cache \
--engine valkey --major-engine-version 9
Two documented gotchas: a single-node cluster-mode-disabled cache must first be wrapped in a replication group, and an upgrade is blocked if there is a pending scale-up. For rollback, any user group attached to the cache must have engine type REDIS. The payoff is on the pricing page: Valkey nodes are 20% cheaper and Valkey serverless 33% cheaper with a 100 MB minimum.
Step 6: Run Valkey on Kubernetes with the official Helm chart
The Valkey project publishes a chart at valkey.io/valkey-helm. Version 0.12.1 ships Valkey 9.1.3 and supports standalone or one primary with replicas. It does not do automatic failover: the primary Service selects pod valkey-0. The operator chart exists too, but its README says it is "not ready for production use". I rendered these values with helm template to confirm them.
# values-valkey.yaml for chart valkey 0.12.1
image:
tag: "9.1.3"
replica:
enabled: true
replicas: 2 # StatefulSet of 3: valkey-0 primary, two replicas
persistence:
size: 8Gi
auth:
enabled: true
usersExistingSecret: valkey-users # key "default" holds the password
aclUsers:
default:
permissions: "~* &* +@all"
resources:
requests: { cpu: 500m, memory: 2Gi }
limits: { memory: 2Gi }
metrics:
enabled: true # redis_exporter sidecar on port 9121
helm repo add valkey https://valkey.io/valkey-helm/
kubectl create namespace cache
kubectl -n cache create secret generic valkey-users --from-literal=default="$VALKEY_PASS"
helm install valkey valkey/valkey --version 0.12.1 -n cache -f values-valkey.yaml
# Chain it behind the old Redis (7.2 or older) for the migration window
# Both variables expand on your machine, not in the pod
kubectl -n cache exec valkey-0 -c valkey -- valkey-cli -a "$VALKEY_PASS" --no-auth-warning \
CONFIG SET primaryauth "$OLD_REDIS_PASS"
kubectl -n cache exec valkey-0 -c valkey -- valkey-cli -a "$VALKEY_PASS" --no-auth-warning \
REPLICAOF old-redis.default.svc.cluster.local 6379
The two chart replicas follow valkey-0, so they receive the data too. After cutover, run REPLICAOF NO ONE on valkey-0. Keep the window short: if valkey-0 restarts mid-migration, the chart regenerates its config as a plain primary. For Redis 7.4 or 8 sources, skip the REPLICAOF and point RedisShake at the valkey Service instead. If you need clustered Valkey on Kubernetes, read my notes on running databases on Kubernetes before writing your own StatefulSet.
Step 7: Verify the data before cutover
Key counts are necessary but not sufficient. Compare INFO keyspace on both sides per database, then sample real keys and compare type, value and TTL. This script does that with redis-py 8.1.0; I ran it against a local pair and it caught a deliberately changed key (MISMATCH b'user:1': value differs, pttl 3599240 vs -1).
#!/usr/bin/env python3
# compare_keys.py: python compare_keys.py redis://old:6379/0 redis://new:6379/0 2000
# Requires: pip install redis==8.1.0 (works against Redis and Valkey)
import sys
import redis
src = redis.Redis.from_url(sys.argv[1])
dst = redis.Redis.from_url(sys.argv[2])
samples = int(sys.argv[3]) if len(sys.argv) > 3 else 1000
def value(r, key, kind):
if kind == b"string": return r.get(key)
if kind == b"hash": return sorted(r.hgetall(key).items())
if kind == b"list": return r.lrange(key, 0, -1)
if kind == b"set": return sorted(r.smembers(key))
if kind == b"zset": return r.zrange(key, 0, -1, withscores=True)
if kind == b"stream": return r.xrange(key, count=100)
return None # module types: compare TYPE only
print(f"dbsize source={src.dbsize()} target={dst.dbsize()}")
bad = 0
for _ in range(samples):
key = src.randomkey()
if key is None:
break
kind = src.type(key)
problems = []
if dst.type(key) != kind:
problems.append(f"type {kind!r} vs {dst.type(key)!r}")
elif value(src, key, kind) != value(dst, key, kind):
problems.append("value differs")
s_ttl, d_ttl = src.pttl(key), dst.pttl(key)
if (s_ttl < 0) != (d_ttl < 0) or abs(s_ttl - d_ttl) > 5000:
problems.append(f"pttl {s_ttl} vs {d_ttl}")
if problems:
bad += 1
print(f"MISMATCH {key!r}: {', '.join(problems)}")
print(f"checked={samples} mismatches={bad}")
sys.exit(1 if bad else 0)
Expect a few TTL mismatches on keys that expire during the run; anything else is a real problem. Also run your application's integration tests against Valkey with production-like traffic for a day if you can, watching latency percentiles rather than averages.
Step 8: Plan the rollback before you need it
Rollback options depend on the path you took:
- Replication or RedisShake: keep the old Redis running, read-only, for the rollback window. Rolling back is pointing clients back at it, minus any writes made on Valkey since cutover. If you need those writes, run RedisShake in reverse (Valkey source, Redis target) during the window.
- ElastiCache: only Valkey 7.2 can roll back to Redis OSS 7.1, in place or by restoring a Valkey 7.2 snapshot as a new Redis 7.1 cache.
- Valkey 9.x anywhere: its
VALKEY080snapshots cannot be read by Redis, and Memorystore does not allow downgrades. After 9.x, rollback means a logical copy.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Valkey log: Can't handle RDB format version 12 (or 13 to 15) |
Source is Redis 7.4 or 8.x; replication and RDB copy are not supported | Use RedisShake (Step 4) instead of REPLICAOF |
ERR DUMP payload version or checksum are wrong on the target |
A tool is sending Redis 7.4+ DUMP payloads via RESTORE |
Set target_redis_proto_max_bulk_len = 0 so values are replayed as commands |
master_link_status:down, auth errors in the Valkey log |
primaryauth/primaryuser missing or wrong for an ACL source |
CONFIG SET primaryuser <user> and CONFIG SET primaryauth <pass>, then recheck |
| RedisShake panics on an unknown type during dry run | Module data (JSON, search, vector sets) or a newer Redis encoding | Exclude those keys with [filter] rules and rebuild them on Valkey |
| Physical RDB copy loads zero keys | AOF is enabled, so Valkey loads the AOF and ignores dump.rdb |
Start Valkey once with appendonly no, then enable AOF |
| ElastiCache upgrade stays blocked | Pending scale-up on the replication group | Apply the scale-up first, or schedule the upgrade with --no-apply-immediately |
| ElastiCache rejects the parameter group | Custom Redis group static parameters differ from the Valkey group | Create a Valkey group with matching static values and pass it |
| Client library disables newer features on Valkey | It reads redis_version, which Valkey pins at 7.2.4 |
Read valkey_version or configure features explicitly |
Replicas grow past maxmemory after a slot migration |
Bug fixed in Valkey 9.1.3 | Upgrade to 9.1.3 or later before migrating slots |
| RDB load failures between 9.0 and 9.1.x | Listpack encoding incompatibility fixed in 9.1.3 | Run 9.1.3 on both sides before mixing versions |
FAQ
Can I migrate Redis 8 to Valkey with replication?
No. Redis 7.4 and later write RDB format 12 or higher, which Valkey treats as foreign, and the Valkey guide says those files are not compatible. Use a logical copy tool such as RedisShake, which parses the source data and replays it as commands.
How long does a Redis to Valkey migration take?
The data copy runs at roughly replication speed, so minutes for a few gigabytes on the same network. The cutover itself is seconds. The real time goes into inventory, module checks and the soak period, which I would budget in days, not hours.
Do I need to change my application code to use Valkey?
Usually not. Valkey speaks the same protocol, so existing clients connect unchanged, and on ElastiCache even the endpoint stays the same. Code that depends on Redis 8 modules or that feature-detects by redis_version is the exception.
Can I roll back from Valkey to Redis?
On ElastiCache, only from Valkey 7.2 to Redis OSS 7.1. Self-hosted, keep the old Redis running and treat it as your rollback target, or copy data back with RedisShake. Valkey 9 snapshots cannot be loaded by any Redis version.
In production, the first thing I check is head -c 9 on every snapshot, because it decides the whole plan in one line. After that I move the least stateful caches first, keep the source alive for at least a week, and stop ElastiCache upgrades at Valkey 7.2 until I trust the soak. Official references: the Valkey migration guide, the ElastiCache cross-engine upgrade docs, the RedisShake documentation and the Valkey Helm charts.
Related: Redis vs Valkey in 2026, Should You Run Databases on Kubernetes in 2026? and How to Fix Helm UPGRADE FAILED Errors.
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Fix AWS ALB Unhealthy Targets and 502/503 Errors (2026)
Fix AWS Application Load Balancer unhealthy targets and 502, 503 and 504 errors step by step: read the health reason code, fix health check paths and success codes, security groups, ip vs instance targets, EKS readiness gates, deregistration delay, keep-alive timeouts and access logs.
13 minNov 10, 2026How to Fix Kubernetes PVC Pending and Multi-Attach Errors (2026)
Fix a Kubernetes PersistentVolumeClaim stuck in Pending and the Multi-Attach (now Waiting for detach) error: missing default StorageClass, EBS CSI driver not installed, zone conflicts, access modes, stuck Terminating and failed resizes, with commands for Kubernetes 1.37.
15 minNov 10, 2026How to Run Kubernetes on Spot Instances: Interruption Handling (2026)
Run EKS workloads safely on Spot in 2026: pick Karpenter 1.15 interruption handling, managed node groups or Node Termination Handler 1.25.6 queue mode, then add PDBs, topology spread, an on-demand floor, short grace periods and an AWS FIS interruption test.
13 min