Redis SCAN vs KEYS: Safe Key Iteration in Production
KEYS * is the most dangerous command you can run in production Redis. It blocks the server while scanning the entire keyspace. This guide shows you the safe alternative: the SCAN family of commands.
The Problem
You need to find all keys matching a pattern. Maybe you're cleaning up expired cache keys, migrating data, or debugging. The obvious approach:
KEYS cache:user:*
This works fine with 100 keys. With 10 million keys, it blocks Redis for seconds — no other commands can execute. Your entire application freezes.
Why KEYS Is Dangerous
KEYS has O(N) time complexity where N is the total number of keys in the database. It runs synchronously, blocking the single-threaded Redis event loop.
SET key:1 "a"
SET key:2 "b"
SET key:3 "c"
KEYS key:*
This looks harmless. But in production with millions of keys, KEYS * can:
- Block Redis for 1–10+ seconds.
- Cause all connected clients to timeout.
- Trigger cascading failures in your application.
- Trip circuit breakers and health checks.
Redis documentation itself says: "Don't use KEYS in production."
SCAN: The Safe Alternative
SCAN iterates the keyspace incrementally using a cursor. It returns a small batch of keys per call and never blocks for long.
Basic Usage
SET user:1001 "Alice"
SET user:1002 "Bob"
SET user:1003 "Charlie"
SET product:2001 "Widget"
SET product:2002 "Gadget"
SET cache:temp:1 "data1"
SET cache:temp:2 "data2"
SCAN 0 COUNT 3
SCAN returns two things:
- A cursor (use it in the next SCAN call).
- A batch of keys.
When the cursor returns 0, the iteration is complete.
Full Iteration
SCAN 0 COUNT 3
Take the cursor from the result and use it in the next call:
SCAN <cursor_from_previous> COUNT 3
Repeat until the cursor returns 0.
Pattern Matching
SCAN 0 MATCH user:* COUNT 10
Returns only keys matching the user:* pattern. But note: SCAN applies the pattern filter AFTER fetching the batch, so some calls may return zero results even when matching keys exist.
TYPE Filtering (Redis 6.0+)
SCAN 0 TYPE string COUNT 10
SCAN 0 TYPE hash COUNT 10
SCAN 0 TYPE zset COUNT 10
Filter by data type. Useful when you only want to find, say, all Hashes.
HSCAN: Iterate Hash Fields
For large Hashes, HGETALL is the equivalent of KEYS * — it blocks. Use HSCAN instead:
HSET user:profile name "Alice" age "30" email "alice@example.com" role "admin" city "Springfield" country "US"
HSCAN user:profile 0 COUNT 2
With Pattern Matching
HSCAN user:profile 0 MATCH *a* COUNT 10
Returns only fields matching the pattern.
SSCAN: Iterate Set Members
SADD tags "redis" "database" "cache" "nosql" "performance" "tutorial" "guide"
SSCAN tags 0 COUNT 3
SSCAN tags 0 MATCH *re* COUNT 10
ZSCAN: Iterate Sorted Set Members
ZADD leaderboard 100 "alice" 200 "bob" 150 "charlie" 300 "diana" 250 "eve"
ZSCAN leaderboard 0 COUNT 2
ZSCAN leaderboard 0 MATCH *a* COUNT 10
Returns members with their scores.
The COUNT Parameter
COUNT is a hint, not a guarantee. It tells Redis approximately how many elements to scan per iteration.
SET a "1"
SET b "2"
SET c "3"
SET d "4"
SET e "5"
SCAN 0 COUNT 2
Redis may return more or fewer than COUNT elements. The default is 10.
Choosing COUNT
- Small databases (< 10K keys):
COUNT 100is fine. - Medium databases (10K–1M keys):
COUNT 1000balances speed and responsiveness. - Large databases (> 1M keys):
COUNT 5000–10000for faster iteration, but monitor latency.
Practical Patterns
Delete Keys by Pattern
The safe way to delete all keys matching a pattern:
SET cache:temp:1 "data1"
SET cache:temp:2 "data2"
SET cache:temp:3 "data3"
SET cache:temp:4 "data4"
SCAN 0 MATCH cache:temp:* COUNT 100
In your application, loop:
SCAN cursor MATCH pattern COUNT 100DEL(orUNLINK) the returned keys.- Repeat until cursor is 0.
DEL cache:temp:1 cache:temp:2
SCAN 0 MATCH cache:temp:* COUNT 100
DEL cache:temp:3 cache:temp:4
Count Keys by Pattern
SCAN 0 MATCH user:* COUNT 100
Accumulate the count across all iterations. There's no built-in "count matching keys" command.
Find Keys Without TTL
Iterate and check each key:
SET permanent:key "no ttl"
SET temp:key "has ttl" EX 300
TTL permanent:key
TTL temp:key
In your script: SCAN, then check TTL for each key. If TTL returns -1, the key has no expiration.
Migrate Keys Between Databases
SCAN 0 MATCH migrate:* COUNT 100
For each key, DUMP + RESTORE to the target database or instance.
Guarantees and Limitations
What SCAN Guarantees
- Every key that exists from start to finish of the iteration will be returned.
- The iteration will eventually complete (cursor returns 0).
What SCAN Does NOT Guarantee
- No duplicates: A key may be returned more than once. Your application must handle deduplication.
- No consistency: Keys added or deleted during iteration may or may not appear.
- Exact COUNT: The number of returned elements per call varies.
Handling Duplicates
SET key:1 "a"
SET key:2 "b"
SET key:3 "c"
SCAN 0 COUNT 2
Use a Set in your application to track seen keys:
# Pseudocode
seen = set()
cursor = 0
while True:
cursor, keys = redis.scan(cursor, count=100)
for key in keys:
if key not in seen:
seen.add(key)
process(key)
if cursor == 0:
break
Performance Comparison
| Command | Time Complexity | Blocking | Production Safe |
|---|---|---|---|
| KEYS * | O(N) | Yes (full scan) | No |
| SCAN | O(1) per call | No (incremental) | Yes |
| HGETALL | O(N) | Yes (full hash) | Risky for large hashes |
| HSCAN | O(1) per call | No (incremental) | Yes |
Pitfalls
- Forgetting to handle cursor 0: The iteration is only complete when the cursor returns
0. Don't stop after a fixed number of calls. - Ignoring duplicates: SCAN can return the same key twice. Always deduplicate in your application.
- Using KEYS in scripts: Even in Lua scripts,
KEYSblocks. UseSCANin your application code instead. - Too small COUNT:
COUNT 1makes iteration extremely slow on large databases. Use at leastCOUNT 100. - Pattern matching overhead: Complex glob patterns (e.g.,
*foo*bar*) are slower. Keep patterns simple. - MATCH returns empty batches: SCAN may return empty arrays even when matching keys exist. This is normal — keep iterating until cursor is 0.
SET user:1 "a"
SET product:1 "b"
SET product:2 "c"
SET product:3 "d"
SCAN 0 MATCH user:* COUNT 2
You might get an empty result if the sampled batch contained only product:* keys. The next SCAN call may find user:1.
When KEYS Is Actually OK
- Development/debugging on a local instance.
- Redis instances with < 1000 keys.
- One-time scripts on a replica (not the primary).
- Inside
redis-clifor quick inspection of small databases.
SET test:1 "a"
SET test:2 "b"
SET test:3 "c"
KEYS test:*
DBSIZE
Try It in the Editor
Head to the Redis Online Editor and practice safe iteration:
SET user:1001 "Alice"
SET user:1002 "Bob"
SET user:1003 "Charlie"
SET product:2001 "Widget"
SET product:2002 "Gadget"
SET cache:temp:1 "data"
SCAN 0 MATCH user:* COUNT 10
SCAN 0 MATCH product:* COUNT 10
SCAN 0 COUNT 3
HSET profile name "Alice" age "30" email "alice@example.com"
HSCAN profile 0 COUNT 2
ZADD scores 100 "alice" 200 "bob" 150 "charlie"
ZSCAN scores 0 COUNT 2
Compare KEYS * with SCAN 0 COUNT 10 — both return all keys, but SCAN does it safely in batches. That's the difference between a smooth production system and a 3 AM incident.