Before you start
This guide copies the keys of your current Redis-compatible server, whether you run it yourself or rent it from a provider, into an ElectraDB Redis-compatible (Valkey) database. It uses about 40 lines of Python and nothing on the server side but standard commands, so it works wherever you can connect to both ends.
What is copied, and what is not
- Every key with its value and its remaining expiry is copied, whatever its type.
- Pub/Sub messages are not data and are not copied; your subscribers simply reconnect to the new database.
- Users, passwords and settings stay behind. An ElectraDB database has one user,
u_<id>, and you choose its eviction policy and replication mode on the database page. - Keys of module types, such as JSON documents, search indexes, time series or Bloom filters, cannot be copied: our Valkey runs without modules.
We do not import an RDB file of your own, and your database cannot be made a replica of your current server. The copy goes through the network, key by key, as described below.
Pick the plan by dataset size
Key-value plans are sold by the size of the dataset they hold (see pricing). On the source, used_memory_human in INFO memory says how much yours takes now:
redis-cli -h <current host> -p 6379 --user <user> --askpass INFO memory | grep used_memory_humanChoose a plan with room above that figure: the same data can take a little more or less memory on another server version, and your dataset will grow. Create the database (New database, then Redis-compatible) and keep its eviction policy at No eviction for the copy. A dataset too large for the plan then shows up as out-of-memory errors in the script, instead of keys silently evicted to make room. You can pick another policy afterwards.
When the database is live, copy its connection string and password from its page. The string has this form:
rediss://u_<id>:<password>@<id>.db.electradb.com:<port>How the copy works
Every Redis-compatible server has three commands that together copy a key exactly:
DUMP keyreturns the key's value in the server's own serialized form, whatever its type: a string, a hash, a list, a set, a sorted set or a stream.PTTL keyreturns the time left before the key expires, in milliseconds:-1when it has no expiry,-2when it no longer exists.RESTORE key ttl payload REPLACEcreates the key from that payload on the target, with the given expiry (0for none).REPLACEoverwrites a key that is already there, so the copy can run as often as you like.
The script walks the source with SCAN, which visits every key in small steps without blocking the server the way KEYS would, and sends the commands in batches of 500 to save round trips. Keys that expire between the scan and the copy are skipped.
RESTORE accepts payloads written by the same or an older version of a Redis-compatible server. Newer servers can write a format Valkey does not read; the target then refuses the key with DUMP payload version or checksum are wrong. The script reports such keys and carries on: see When a key is refused.
The script
It needs Python 3 and redis-py:
pip install redis# copy_keys.py: every key, value and expiry from SOURCE_URL to TARGET_URL
import os
import sys
import redis
src = redis.from_url(os.environ["SOURCE_URL"], socket_connect_timeout=10, socket_timeout=60)
dst = redis.from_url(
os.environ["TARGET_URL"], ssl_check_hostname=True, socket_connect_timeout=10, socket_timeout=60
)
BATCH = 500
def copy(keys):
"""DUMP and PTTL each key on the source, RESTORE it on the target. Returns (copied, failed)."""
read = src.pipeline(transaction=False)
for key in keys:
read.pttl(key)
read.dump(key)
found = read.execute()
write = dst.pipeline(transaction=False)
sent = []
for key, ttl, payload in zip(keys, found[0::2], found[1::2]):
if ttl == -2 or payload is None:
continue # expired or deleted since the scan
write.restore(key, ttl if ttl > 0 else 0, payload, replace=True)
sent.append(key)
failed = 0
for key, result in zip(sent, write.execute(raise_on_error=False)):
if isinstance(result, Exception):
failed += 1
print(f"{key!r}: {result}", file=sys.stderr)
return len(sent) - failed, failed
copied = failed = 0
batch = []
for key in src.scan_iter(count=1000):
batch.append(key)
if len(batch) == BATCH:
c, f = copy(batch)
copied, failed, batch = copied + c, failed + f, []
if batch:
c, f = copy(batch)
copied, failed = copied + c, failed + f
print(f"copied {copied} keys, {failed} refused")The target URL uses rediss://, so the connection is encrypted, and the certificate and the host name are checked against the certificate authorities your system already trusts. Use rediss:// for the source too if it requires TLS.
Run it and check the result
Put both URLs in the environment, with the passwords URL-encoded if they contain characters such as @ or / (the database page shows the target's already encoded), and run the script from a machine close to the database's region:
export SOURCE_URL='redis://<user>:<password>@<current host>:6379/0'
export TARGET_URL='rediss://u_<id>:<password>@<id>.db.electradb.com:<port>'
python copy_keys.pyThe number at the end of the source URL is the logical database it reads, and the target URL selects database 0. If INFO keyspace on the source lists keys in other databases than db0, run the script again for each, with that number at the end of both URLs.
Then compare the two sides. DBSIZE counts the keys; the numbers match when nothing wrote to the source during the copy and no key expired in between:
redis-cli -h <current host> -p 6379 --user <user> --askpass DBSIZE
redis-cli --tls -u "rediss://u_<id>:<password>@<id>.db.electradb.com:<port>" DBSIZESpot-check a few keys of each kind your application uses on both sides: TYPE, PTTL, and the value itself (GET, HGETALL, LRANGE key 0 -1). INFO memory on the target shows how much of the plan's dataset size the copy takes. Then point a staging copy of your application at the new database and run your tests.
When a key is refused
If the target refuses keys with DUMP payload version or checksum are wrong, your source writes a newer format than Valkey reads. Copy those keys by type instead, with the commands your Redis clients already use. This function covers the common types; call it for each key the script reported:
def copy_by_type(key):
kind = src.type(key).decode()
ttl = src.pttl(key)
pipe = dst.pipeline() # MULTI/EXEC: the key appears complete or not at all
pipe.delete(key)
if kind == "string":
pipe.set(key, src.get(key))
elif kind == "hash":
pipe.hset(key, mapping=src.hgetall(key))
elif kind == "list":
pipe.rpush(key, *src.lrange(key, 0, -1))
elif kind == "set":
pipe.sadd(key, *src.smembers(key))
elif kind == "zset":
pipe.zadd(key, dict(src.zrange(key, 0, -1, withscores=True)))
else:
raise ValueError(f"{key!r}: copy {kind} keys another way")
if ttl > 0:
pipe.pexpire(key, ttl)
pipe.execute()Streams need XRANGE and XADD with their entry IDs, and their consumer groups have to be created again. If you already know a migration tool such as RIOT, it can copy by type as well.
Switch your application over
The first run is the rehearsal: it shows whether every key is accepted, whether the plan is large enough, and how long a full copy takes. For the real move:
- Freeze writes to the source: stop the workers and jobs that write, or put the application in maintenance mode.
- Empty the target with
FLUSHDB. The script overwrites keys that exist on both sides, but a key deleted from the source since the rehearsal would otherwise stay on the target. - Run the script again and compare
DBSIZEon both sides. - Switch the connection string in your application to the ElectraDB one and start it again.
The freeze lasts about as long as the rehearsal's copy took. If that is too long for you, you can instead run a final pass during a shorter freeze without emptying the target, and accept that keys deleted from the source in the meantime remain until they expire or you delete them.
Afterwards, set a connect timeout of 5 to 10 seconds in your client, as the database page's examples do, and choose the replication mode on high-availability plans. Your data is then archived every 30 seconds and snapshotted daily; see Backups and point-in-time restore and MariaDB and Redis-compatible databases are here for what the database keeps for you. To move a database from a MySQL-compatible server as well, see the MariaDB migration guide.
More articles
- Coming soon: databases that move closer to your applicationTell us where your application runs and we will place your database, and over time move it, to the servers with the lowest latency to it.
- Migrating a MySQL or MariaDB database to ElectraDBDump a MySQL or MariaDB database, load it into ElectraDB's MariaDB over verified TLS, and switch your application over with a short write freeze.
- MariaDB and Redis-compatible databases are hereElectraDB now runs MariaDB (MySQL-compatible) and Redis-compatible Valkey databases, with the same failover, backups and restore as PostgreSQL.
- All articlesEvery article, newest first.