KEYS command in Redis is a synchronous operation that iterates over the entire keyspace and returns all keys matching a pattern. If the keyspace is large, this operation can take a significant amount of time, during which Redis will be unable to process any other commands, effectively blocking your entire production environment. SCAN is an iterative, cursor-based alternative that allows you to retrieve keys in chunks without blocking the server.
Let’s see KEYS in action, or rather, not in action, because that’s the point. Imagine a Redis instance with 10 million keys, and you run KEYS *:
redis-cli KEYS *
# This command will run for seconds, minutes, or even longer,
# during which no other commands like SET, GET, DEL, or even other KEYS/SCAN
# will be processed. Your application will hang.
Now, let’s look at SCAN. We start with a cursor of 0:
redis-cli SCAN 0
# Output: ["12345", ["key1", "key2", "key3"]]
The output is a two-element array: the new cursor (here, 12345) and an array of keys found in this iteration. To continue scanning, you use the new cursor:
redis-cli SCAN 12345
# Output: ["67890", ["key4", "key5"]]
You keep calling SCAN with the returned cursor until the cursor returns to 0.
redis-cli SCAN 67890
# Output: ["0", ["key6", "key7"]]
The problem KEYS solves is simple: "Give me all the keys that match this pattern." But the mechanism it uses is a full-table scan of Redis’s internal hash table that stores keys. This is inherently blocking. When KEYS is executing, Redis’s main thread is dedicated to traversing this table. It cannot serve any other requests until KEYS is finished. This is acceptable for small datasets or in development, but in production, even a few milliseconds of blocking can cascade into timeouts and failures across your application.
SCAN addresses this by breaking the keyspace traversal into small, manageable chunks. It uses an internal cursor to keep track of its progress. On each SCAN call, it scans a portion of the keyspace and returns a subset of keys along with the next cursor to use. This ensures that no single SCAN operation takes a significant amount of time, keeping Redis responsive. The SCAN command also has parameters for MATCH (similar to KEYS pattern matching) and COUNT (a hint to Redis about how many keys to return per iteration, not a guarantee).
# Example with MATCH and COUNT
redis-cli SCAN 0 MATCH "user:*" COUNT 100
# Output: ["54321", ["user:101", "user:102", ...]]
The COUNT parameter is crucial for managing the load. A higher COUNT might reduce the number of SCAN calls needed but increases the work done in a single call. A lower COUNT means more calls but less work per call. The optimal value depends on your Redis instance’s load and the desired responsiveness. Experimentation is key here.
It’s vital to understand that SCAN is not guaranteed to return all keys, nor is it guaranteed to return keys only once. It’s possible for SCAN to miss keys that were added after the cursor passed them, or to return keys multiple times if they are added and deleted within the scan window. The Redis documentation states that SCAN guarantees that the client will encounter every key in the database at least once, but it doesn’t guarantee that it won’t encounter keys multiple times. For most use cases, like iterating to delete old keys or to get a sample of data, this "at least once" guarantee is sufficient. If you need exactly-once processing or a perfectly consistent snapshot, you’d need to implement more complex logic, potentially involving taking a Redis snapshot (RDB) and scanning that, or using other synchronization mechanisms.
The COUNT parameter is not a strict limit on the number of keys returned; it’s a hint to the Redis server. Redis will try to return approximately COUNT keys per iteration, but the actual number can vary significantly. This is because Redis iterates over its internal hash table, and the number of keys it finds within a given "slice" of that table might be more or less than the COUNT hint. The server prioritizes completing the iteration quickly over strictly adhering to the COUNT.
The next problem you’ll likely encounter is handling the case where a key is modified or deleted between two SCAN calls, leading to potential inconsistencies if your application logic assumes a static dataset.