Renaming and Scanning Keys
As an application grows, you’ll often need to reorganize the keys living in your Redis keyspace — fixing a naming mistake, migrating to a new naming scheme, or promoting a temporary key to a permanent one. Redis gives you RENAME and RENAMENX for that. Separately, once your keyspace has thousands or millions of keys, you’ll need a safe way to browse or audit it — that’s what SCAN is for, as a non-blocking alternative to the dangerous KEYS command. This lesson covers both: renaming keys correctly (including a TTL gotcha that surprises a lot of people) and scanning a keyspace the right way.
Overview / How it works
RENAME key newkey changes a key’s name in place. Internally, Redis’s keyspace is a hash table mapping key names to values, so a rename is just a dictionary operation: remove the entry under the old name, insert it under the new name, pointing at the same underlying value object. No data is copied or serialized, which is why it’s O(1) regardless of how large the value is (a huge hash or list renames just as fast as a tiny string). Because Redis is single-threaded, this remove-then-insert happens atomically — no other client can ever observe a moment where the key exists under neither name or both names.
A critical detail: if newkey already exists, RENAME silently overwrites it and deletes whatever was there. If you don’t want that behavior, use RENAMENX (“rename if not exists”), which refuses the rename and returns 0 when the destination is already taken, leaving both keys untouched.
TTLs travel with the rename. If the source key had an expiration set, the destination key inherits that exact same TTL after the rename; if the source had no TTL, the destination ends up with no TTL — even if the old destination key previously had one. This trips people up constantly, so we’ll dedicate an example to it below.
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] is a cursor-based iterator over the keyspace. Unlike KEYS, which walks the entire hash table in one blocking pass and returns everything at once, SCAN walks a small, bounded slice of the hash table’s internal buckets on each call and immediately returns control to the single-threaded server, so other commands can run in between. You keep calling SCAN with the cursor it just returned until it returns cursor 0, meaning the full iteration completed. Because the keyspace can be modified by other clients while you’re mid-scan, SCAN offers a weaker guarantee than a snapshot: it guarantees every key present for the entire duration of the scan will be returned at least once, but keys added or removed during the scan may or may not appear, and a key can occasionally be returned more than once. For almost every real use case — auditing, exporting, finding candidates for cleanup — that’s a perfectly acceptable tradeoff for not locking up the server.
Syntax
RENAME key newkey
RENAMENX key newkey
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
key— the existing key you want to rename.newkey— the target name. ForRENAMEthis overwrites an existing key of the same name; forRENAMENXthe operation fails (returns0) if this name is already taken.cursor— start iteration with0; on each call use the cursor value SCAN just returned. A returned cursor of0means iteration is finished.MATCH pattern— optional glob-style filter (e.g.user:*) applied to the keys found in that pass. Filtering happens after the internal scan step, so it does not change the O(1)-per-call cost.COUNT count— a hint for how many internal buckets to visit per call (default 10). It is not a hard cap on the number of keys returned — treat it as a rough sizing knob, not a limit.TYPE type— optional filter to only return keys of a given data type (e.g.string,hash,list).
| Command | Time Complexity | Notes |
|---|---|---|
RENAME |
O(1) | Errors if source key doesn’t exist |
RENAMENX |
O(1) | No-op (returns 0) if destination exists |
SCAN |
O(1) per call, O(N) for a full iteration | Non-blocking, cursor-based |
KEYS |
O(N) | Blocks the server for the entire pattern match — avoid in production |
Examples
Example 1: Basic rename.
SET user:1001:name "Ada"
RENAME user:1001:name user:1001:fullname
GET user:1001:fullname
EXISTS user:1001:name
Output:
OK
OK
"Ada"
(integer) 0
The value moves to the new key name and the old name no longer exists.
Example 2: RENAME silently overwrites the destination.
SET session:abc123 "active"
SET session:xyz789 "expired"
RENAME session:abc123 session:xyz789
GET session:xyz789
EXISTS session:abc123
Output:
OK
OK
OK
"active"
(integer) 0
The old value "expired" that was under session:xyz789 is gone forever — RENAME overwrote it without warning. This is the exact scenario RENAMENX protects against.
Example 3: RENAMENX refuses to clobber an existing key.
SET cache:page:home "v1"
SET cache:page:about "v2"
RENAMENX cache:page:home cache:page:about
GET cache:page:home
GET cache:page:about
Output:
OK
OK
(integer) 0
"v1"
"v2"
RENAMENX returned 0 because cache:page:about already existed, so nothing changed — both keys still hold their original values. Compare with a destination that’s free:
SET cache:page:contact "v3"
RENAMENX cache:page:contact cache:page:contact-us
GET cache:page:contact-us
Output:
OK
(integer) 1
"v3"
Example 4: TTL travels with the key on rename.
SET session:tok1 "data" EX 100
TTL session:tok1
RENAME session:tok1 session:tok2
TTL session:tok2
Output:
OK
(integer) 100
OK
(integer) 100
The TTL that was on session:tok1 moved with it to session:tok2 — the key didn’t lose its expiration just because it changed names.
Example 5: Scanning a keyspace with MATCH and TYPE.
MSET product:1:name "Widget" product:2:name "Gadget" product:3:name "Gizmo" order:1:status "shipped"
SCAN 0 MATCH product:* COUNT 1000
Output:
OK
1) "0"
2) 1) "product:1:name"
2) "product:2:name"
3) "product:3:name"
With a small keyspace and a generous COUNT, a single call is enough to finish the whole iteration (cursor comes back as "0"). On a real production dataset with millions of keys, the same call would return a nonzero cursor and only a slice of matches, and you’d loop until the cursor is 0 again. Note that SCAN‘s result order is not guaranteed and can differ from what’s shown here. You can also filter by data type instead of (or in addition to) a name pattern:
SET counter:visits 100
LPUSH queue:jobs job1 job2
SCAN 0 TYPE string
Output:
OK
(integer) 2
1) "0"
2) 1) "counter:visits"
queue:jobs is a list, so TYPE string excluded it from the results even though its name doesn’t match any pattern filter.
How it works step by step
For RENAME key newkey: (1) Redis looks up key in the keyspace hash table — if it isn’t found, it returns an error immediately. (2) If newkey already exists, Redis removes that entry (and its value) first. (3) Redis removes the key entry and reinserts the same value object under newkey, carrying over the expiration timestamp associated with the original key, if any. (4) It signals a keyspace notification and returns OK. Every one of these steps happens within a single atomic command execution because the main thread never interleaves other commands mid-operation.
For SCAN cursor: Redis’s hash table grows by doubling and uses a bucket-indexing scheme designed so that a cursor value encodes exactly which buckets have been visited so far, even if the table resizes mid-scan. Each call visits a small number of buckets (governed by COUNT), collects whatever keys live there, applies your MATCH/TYPE filters, and returns both the results and an updated cursor pointing at the next unvisited buckets. Because each call touches only a bounded number of buckets, no single call can block the server for long, no matter how big the overall keyspace is.
Common Mistakes
Mistake: using KEYS * (or any broad pattern) on a large production database. KEYS is O(N) and, being a single command, runs to completion before Redis processes anything else — on a keyspace with millions of keys this can stall every other client for seconds. Always use SCAN in application code or ad-hoc production inspection instead; reserve KEYS for small development databases.
Mistake: assuming RENAME is safe to run without checking the destination first. As Example 2 showed, RENAME silently deletes whatever was at newkey. If you only want the rename to happen when the destination is free, use RENAMENX and check its return value (1 = renamed, 0 = destination already existed, nothing changed).
Mistake: renaming a key that doesn’t exist. This is a real Redis error, not a no-op:
RENAME nokey:here anotherkey
Output:
(error) ERR no such key
Always confirm the source key exists (e.g. with EXISTS) if there’s any chance it might have expired or never been set, especially in scripts that shouldn’t halt on an error.
Best Practices
- Default to
RENAMENXwhen you’re not certain the destination name is free — it fails safe instead of silently destroying data. - Remember TTLs move with a
RENAME— if you rename a key that should now be permanent, you may need to explicitly clear its expiration afterward (e.g. withPERSIST). - Never run bare
KEYS patternagainst a production instance of any meaningful size; useSCANwith a reasonableCOUNTinstead. - When scanning, always loop until the returned cursor is
0— stopping after one call only processes a partial slice of the keyspace, not the whole thing. - Use
SCAN ... MATCH patternto narrow results to a naming convention (e.g.session:*) rather than pulling everything and filtering client-side. - Use
SCAN ... TYPE typewhen you only care about one data type, to avoid extra client-side type checks. - Treat
SCAN‘s guarantees correctly: it’s suitable for maintenance, auditing, and cleanup tasks, but not for taking a perfectly consistent snapshot of a keyspace under heavy concurrent writes.
Practice Exercises
- You accidentally created a key named
usr:200:mailinstead ofuser:200:email. Rename it to the correct name, and confirm the old name no longer exists and the new one holds the original value. - Create two keys,
report:draftandreport:final, with different string values. Try to renamereport:draftontoreport:finalusing the command that refuses to overwrite an existing destination, and confirm neither key’s value changed. - Populate five keys under the prefix
metric:and one key underdebug:temp. UsingSCANwith a match pattern, iterate until the cursor returns to0and confirm only themetric:keys are returned, notdebug:temp.
Summary
RENAME key newkeyrenames a key in O(1) time and overwrites the destination if it already exists.RENAMENX key newkeydoes the same rename but refuses (returns0) if the destination already exists, protecting existing data.- A key’s TTL travels with it through a rename; a key with no TTL produces a destination with no TTL, even if the destination previously had one.
- Renaming a nonexistent source key returns a real Redis error (
ERR no such key), not a silent no-op. SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]iterates the keyspace incrementally without blocking the server; keep calling it with the returned cursor until it comes back as0.- Never use
KEYS patternon a nontrivial production keyspace — its O(N) blocking scan can stall every other client; useSCANinstead.
