$ python3 tools/keymove.py --apply --match 'doc:*'
moved 0 keys, skipped 0, in 4.2s
14 keys refused by the target (use --replace only if you mean it):
doc:1041: DUMP payload version or checksum are wrong
doc:1042: DUMP payload version or checksum are wrong
That is not corruption, and --replace will not fix it. It is a RESTORE on a
server whose RDB format is older than the one that produced the payload, or a
value whose type belongs to a module the target does not have. Both are
knowable before you write a single key, which is what this repository is for.
Tooling and an honest comparison for people considering a move off Redis Cloud (or Redis Enterprise, or any managed Redis) onto a plain Redis 7.2 server — including the free instances at freebase.cloud, which is what this repo was built against.
The order of business here is deliberately backwards from most migration guides. Feasibility first, verification second, the actual copy last, because the copy is the easy part.
| 1. Find out if you can move | the module scan, and why MODULE LIST is not enough |
| 2. Move the keys | DUMP → RESTORE, and getting TTLs right |
| 3. Prove it worked | counts, type histogram, sampled deep compare |
| 4. Roll it back | manifest-driven, not FLUSHDB |
| What Redis Cloud does that a plain server does not | the fair part |
| Stay where you are if… | four real reasons |
| Running Redis on freebase.cloud | connection details and MCP |
DUMP/RESTORE moves values, not capabilities. Two things break it, and
both are invisible until you try:
Module-owned key types. A key holding a JSON document has type
ReJSON-RL, not string. A server without that type loaded cannot decode the
payload, and there is no conversion — the document has to be re-derived from
whatever wrote it. The same applies to TSDB-TYPE, MBbloom--, MBbloomCF,
CMSk-TYPE, topk-TYPE, TDIS-TYPE and Redis 8's vectorset.
RDB payload version. Every DUMP payload ends with a 2-byte RDB version
and an 8-byte CRC64. RESTORE checks both and errors if the version is higher
than it understands (RESTORE
docs). Moving from a Redis 8
source to a Redis 7.2 target can therefore fail on every single key while both
servers report themselves perfectly healthy.
pip install 'redis>=5.0'
export SOURCE_REDIS_URL='rediss://default:PASSWORD@redis-12345.example.com:12345'
export TARGET_REDIS_URL='redis://HOST:6379/0'
python3 tools/module_scan.pysource redis 7.4.1
source dbsize 184203 keys
MODULE LIST empty or restricted
FT._LIST idx:products, idx:orders
type sample 20000 keys scanned
hash 11402 core type
string 6110 core type
ReJSON-RL 2488 JSON documents (RedisJSON / Redis 8 JSON)
source RDB ver 12
target redis 7.2.3
target RDB ver 11
BLOCKED — do not start a key migration yet:
- 2488 keys of type ReJSON-RL — JSON documents (RedisJSON / Redis 8 JSON)
- 2 query-engine index(es); FT.* queries will not resolve
- source serializes at RDB 12, target parses up to 11; RESTORE will reject every payload
Note what happened in the middle of that: MODULE LIST came back empty because
plenty of providers restrict it, so the scan falls back to sampling key types,
which cannot be restricted. And the RDB version was not read from INFO — it
is not there. The tool writes one throwaway key with a 60-second TTL, DUMPs
it, reads the version out of the payload footer, and deletes it.
Point TARGET_REDIS_URL at the server you actually intend to land on —
a free Redis 7.2 instance is enough to
run the scan against. Exit code 1 means blocked, 0 means clear, 2 means it could not
connect.
python3 tools/keymove.py --dry-run --match 'session:*'
python3 tools/keymove.py --apply --match 'session:*'SCAN in batches, one pipeline per batch for DUMP plus the expiry, one
pipeline to RESTORE on the far side. No KEYS, no MIGRATE (which needs the
source to be able to open a connection to the target — usually impossible
across two managed providers), and no blocking commands.
The part worth reading closely is expiry:
PEXPIRETIME(Redis 7.0+) returns the absolute expiry in milliseconds. That value is passed toRESTORE … ABSTTL, so a key with 40 seconds left at the start of the run still has about 40 seconds left when it lands, however long the transfer took. On a pre-7.0 source the tool falls back to relativePTTLand prints a warning that expiries will drift by the run duration.-1means no expiry;RESTOREwants0for that. Passing-1through is an error, and passing the raw value where0was meant turns every session token into a permanent key. The mapping is explicit in the code and covered by the verification pass.-2means the key vanished betweenSCANandDUMP. Counted as skipped, not as an error — in a live cache that number is never zero.- An absolute expiry already in the past is skipped rather than restored. A migration should not resurrect data the source had already given up on.
Keys that already exist on the target
come back as BUSYKEY errors and are
reported per key. --replace exists but is not the default, because deciding
which side is authoritative is a judgement call and not a flag.
python3 tools/keymove.py --verify --match 'session:*' --sample 200Three layers, cheapest first:
- Key counts on both sides for the pattern.
- A full type histogram —
string/hash/list/set/zset/streamcounts compared side by side. Catches a partial run that a raw count would hide. - A random sample of N keys deep-compared by type: strings byte for byte, hashes and sets sorted then compared, lists and streams in order, sorted sets with scores. Then TTL: presence must match, and remaining time must agree within five seconds.
Non-zero exit on any mismatch. VERIFY OK is the only output that means
anything.
--apply appends every key it actually wrote to keymove-manifest.jsonl. The
rollback deletes exactly those keys:
python3 tools/keymove.py --undo keymove-manifest.jsonlNot FLUSHDB. If the target database is shared with anything else — and on a
free instance it often is, because you
were experimenting in it last week —
that difference is the whole reason the manifest exists. Keep it until the old
instance is decommissioned, not until the cutover.
redis-cli -u "$SOURCE_REDIS_URL" --rdb snapshot.rdbA full RDB snapshot is faster than key-by-key for large keyspaces, but it is a
replication operation: the client issues SYNC/PSYNC, which managed
providers frequently restrict on shared plans. If it fails with a permission
error, that is a policy decision on the provider's side, not a bug in your
command. keymove.py exists because that path is not always open, and because
restoring an RDB requires filesystem access to the target's data directory —
which a managed target does not give you either.
Last verified: 2026-08-18. Follow the links rather than trusting this table in six months.
| Capability | Redis Cloud / Enterprise | Plain Redis 7.2 server |
|---|---|---|
| Active-Active geo-replication (CRDTs) | Yes, on a Pro subscription — multi-region, conflict-free types, eventual consistency | No. Async replication only, single writer |
| Query engine, JSON, Time Series, probabilistic types | Available as database capabilities | Not in 7.2. Redis Open Source 8 bundles them — a 7.2 server does not have them |
| Clustering / resharding | Managed, with online scaling | Redis Cluster exists in open source but you operate it |
| Backups, failover, patching | Operated for you | Yours |
| Free plan | 30 MB Essentials plan, documented at redis.io | n/a |
Three things worth saying plainly, because comparison tables tend to bury them:
- Active-Active is genuinely hard. CRDT-backed multi-region writes with automatic conflict resolution are not something you assemble from primitives over a weekend. Redis's own docs note the memory cost — as much as 4x the original data size — and that is the honest price of the feature, not a criticism of it.
- The module story changed in 2025. Redis 8 returned to an OSI-approved
licence (AGPLv3) and folded the query engine,
JSON, time series and probabilistic structures into Redis Open Source. If
your blocker is "I need
FT.SEARCHand I don't want a paid plan", the answer today may be Redis 8 rather than a different vendor. It is not Redis 7.2, which is what freebase.cloud runs and what this tooling targets. - A free tier is not a cluster. Nothing in this repo replaces a multi-node Redis Enterprise deployment. It replaces the part of your Redis usage that is a cache, a queue, a leaderboard, or a scratchpad.
- You run Active-Active. There is no migration path in this repo for a multi-region CRDT database, and there is no honest way to fake one. Writing to two independent Redis servers and hoping is not the same feature.
FT.SEARCH,TS.*orJSON.*appear anywhere in your code. The module scan will tell you this in about four seconds. Moving the core types and leaving the module-backed ones behind gives you two sources of truth, which is strictly worse than one expensive one.- You are past the point where 30 MB was the constraint. If your working set is tens of gigabytes with a throughput floor, you are buying operated memory and failover, and that is a reasonable thing to buy.
- Someone is on call for this. A managed provider with a support contract is a load-bearing part of an incident response plan. A free instance is not, and this repo will not pretend otherwise.
Migrating a cache is low risk. Migrating a system of record that happens to live in Redis is not. Know which one you have before you start.
Sign up on the console, create a session, pick the Redis engine. Details on the free Redis instance page.
You get Redis 7.2.3 over the real
RESP2 wire protocol on port 6379 —
redis-cli, redis-py, ioredis, node-redis, Lettuce and go-redis
connect unmodified, which is why the tooling here is ordinary client code with
no vendor SDK in it.
redis-cli -h HOST -p 6379Core data structures, pub/sub, streams, Lua via EVAL, MULTI/EXEC, and the
full expiry command set are all present. Modules are not — that is the
constraint the scan in step 1 exists to surface early.
The free tier suits development, prototyping and small production workloads. No credit card, and no cluster to size.
A second address onto the same keyspace, for AI assistants that speak the Model Context Protocol. Settings → MCP → New Token, pick your connection, copy the URL.
claude mcp add --transport http kv https://freebase.cloud/api/mcp/YOUR_TOKENOr commit .mcp.json with the project:
{ "mcpServers": { "kv": { "type": "http", "url": "https://freebase.cloud/api/mcp/YOUR_TOKEN" } } }The type field is not optional in Claude Code — a url entry without it is a
hard error. The token sits in the path, so there is no Authorization header
to configure anywhere.
Four tools show up, prefixed with the connection name you chose:
| Tool | Use |
|---|---|
kv_query |
run a read command and get the result back |
kv_store |
write keys |
kv_list_tables |
enumerate the keyspace |
kv_annotate_table |
attach a description so the model knows what sess:* means |
Step-by-step client setup: how to connect Claude to Redis. Transport is Streamable HTTP, current under MCP spec revision 2026-07-28; the older HTTP+SSE transport is deprecated.
tools/module_scan.py feasibility: module types, FT indexes, RDB versions
tools/keymove.py dry-run / apply / verify / undo
examples/preflight.sh the three checks, in order, before any write
examples/post_cutover_smoke.mjs six client-level assertions after the move
- RESTORE command reference — TTL semantics,
ABSTTL,REPLACE, the version and checksum check - Active-Active on Redis Cloud
- Redis Cloud Essentials plan details
- Redis 8 GA and the AGPLv3 announcement
- Model Context Protocol
- Redis 7.2 on freebase.cloud
MIT licensed. Issues and pull requests welcome, particularly bug reports against the TTL handling — that is the part where a subtle mistake is most expensive.
freebase.cloud is an independent service and is not affiliated with Redis Ltd. or Anthropic.