Why KEYS Is Dangerous in Production
The KEYS command finds every key in your Redis database that matches a pattern, and it looks completely harmless the first time you try it — it returns instantly, prints a tidy list, and feels like a normal query. On a real production dataset with millions of keys, that same command can freeze every client talking to Redis for seconds at a time. This lesson explains exactly why that happens, and what to use instead.
Overview / How it works
Redis runs its core command processing on a single thread. Every command you send — GET, SET, HSET, KEYS — is executed to completion, one at a time, before the next command even begins. This is actually one of Redis’s biggest strengths: because nothing can interleave mid-command, every individual command is inherently atomic, and you never need explicit locks to increment a counter or update a hash safely. But that same design means a command’s cost is not just about correctness — it’s about how long it monopolizes the only thread doing the work.
KEYS pattern walks the entire keyspace — every key in the database — checking each one against your glob-style pattern, and only then returns the full result as one array. Internally, Redis’s keyspace is a hash table, so KEYS has to iterate every bucket and every entry in it, in whatever order they happen to sit in memory (never insertion order, and not sorted). Its time complexity is O(N), where N is the total number of keys in the database — not the number of keys that match your pattern. A database with 20 million keys pays the full O(N) cost even if your pattern matches only three of them. Because the single thread is busy walking the whole keyspace, no other command from any other client can run until KEYS finishes — not a GET, not a health-check PING, nothing. On a small dev database this takes microseconds and you’d never notice. On a large production instance it can take seconds, during which your entire application appears to hang.
SCAN solves the same problem — “find keys matching a pattern” — without the blocking behavior. Instead of returning everything in one shot, SCAN takes a cursor and returns a small batch of keys plus a new cursor, letting you walk the keyspace incrementally across many small, cheap calls. Each individual SCAN call only inspects a bounded number of hash table buckets (controlled by COUNT, a hint rather than a hard limit), so it returns quickly and lets other commands run in between calls. The tradeoff: SCAN only guarantees that a key present for the entire iteration will be returned at least once — it may return duplicates, and keys added or removed mid-scan may or may not appear. For interactive lookups and admin scripts, that’s a perfectly acceptable tradeoff for not freezing the server.
Syntax
KEYS pattern
pattern— a glob-style pattern, e.g.*(everything),user:*(prefix match),user:100?(single-character wildcard), oruser:[1-3]000(character ranges).
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor— start iteration with0; on each call, pass back the cursor value Redis just returned to you, and stop once it returns0again.MATCH pattern— optional glob pattern; filtering still happens after Redis reads a batch, so a non-matching batch can still return zero keys and a nonzero cursor.COUNT count— a hint for how many keyspace entries to examine per call (default 10), not a guarantee of how many keys will be returned.TYPE type— optional filter to only return keys of a given type, e.g.stringorhash.
Examples
Example 1: KEYS on a small dataset. On a tiny keyspace like this one, KEYS looks perfectly reasonable — which is exactly why it’s an easy trap to fall into during development.
SET user:1001:name "Ada"
SET user:1002:name "Grace"
SET user:1003:name "Linus"
KEYS user:*
Output:
OK
OK
OK
1) "user:1002:name"
2) "user:1001:name"
3) "user:1003:name"
Notice the order isn’t 1001, 1002, 1003 — KEYS returns entries in whatever order the underlying hash table stores them, which has nothing to do with insertion order or sort order. Never rely on the order of a KEYS result.
Example 2: The same search with SCAN. This is the safe, production-friendly equivalent.
SET session:abc123 "active"
SET session:def456 "active"
SET session:ghi789 "active"
SCAN 0 MATCH session:* COUNT 100
Output:
OK
OK
OK
1) "0"
2) 1) "session:def456"
2) "session:abc123"
3) "session:ghi789"
The first element of the reply, "0", is the next cursor. Because our COUNT of 100 was larger than the whole (tiny) test keyspace, Redis finished in a single call and handed back a cursor of 0, meaning the iteration is complete. On a real production database, a single SCAN call would return a nonzero cursor, and your application would keep calling SCAN with that cursor — each call cheap and non-blocking — until it eventually sees 0 again.
Example 3: checking size without walking the keyspace. If all you actually need is a count, don’t use KEYS or even SCAN — Redis tracks the total key count for you.
SET product:5001:price "19.99"
SET product:5002:price "24.50"
DBSIZE
KEYS order:*
Output:
OK
OK
(integer) 2
(empty array)
DBSIZE is O(1) — Redis maintains this count internally and never has to scan anything to report it. The last line shows what you get when a KEYS pattern matches nothing at all: an empty array, not an error and not (nil).
How it works step by step
- The client sends
KEYS pattern(or, internally, one call in aSCANloop). - Redis’s single command-processing thread begins iterating the main keyspace hash table bucket by bucket.
- For
KEYS: every single entry in the table is visited and pattern-matched before anything is returned — the thread cannot service any other client’s command during this entire pass. ForSCAN: only a bounded number of buckets (perCOUNT) are visited before the call returns, so the thread is free to interleave other clients’ commands between successiveSCANcalls. - Matching key names are collected into a reply array and sent back to the client — for
KEYS, all in one round trip; forSCAN, one small batch plus a cursor per round trip. - The client repeats the process (only relevant for
SCAN) by sending the returned cursor back in, until Redis returns a cursor of0, signaling the full iteration is complete.
Common Mistakes
Mistake 1: Running KEYS * against a production master “just to check something.” It feels like a harmless read, but on a database with millions of keys it can block every other client — including your application’s own traffic — for seconds. Use SCAN (or redis-cli --scan from the command line, which wraps the same cursor loop for you) for any ad-hoc lookup on a live server.
Mistake 2: Calling KEYS inside application request-handling code, as if it were an O(1) lookup like GET. Because its cost scales with total keyspace size, a pattern that’s fast today at 10,000 keys can bring the server to a crawl once the dataset grows to 10 million — and the bug won’t show up in testing, only in production under load. Replace it with SCAN, or better, redesign the access pattern so you don’t need to search by pattern at all (e.g. maintain a Set of the key names you care about).
Mistake 3: calling KEYS with no pattern argument, forgetting that the pattern is required, not optional.
KEYS
Output:
(error) ERR wrong number of arguments for 'keys' command
The fix is simply to always supply a pattern — use * explicitly if you really do mean “every key” (and even then, prefer SCAN 0 MATCH * in production):
KEYS *
Best Practices
- Never run
KEYSagainst a production instance with a nontrivial dataset — useSCAN, or runredis-cli --scan --pattern "…"from a shell, which iterates for you automatically. - If you only need a total count, use
DBSIZE(O(1)) instead of counting aKEYSresult. - In application code, avoid pattern searches entirely where possible — maintain an explicit Set or Sorted Set of key names you need to enumerate, so lookups stay O(1) or O(log N) instead of O(N).
- When you do use
SCAN, keepCOUNTmodest (tens to low hundreds) so each call stays cheap, and always loop until the cursor returns to0— stopping early silently skips part of the keyspace. - Remember
SCAN‘s only guarantee is that keys present for the full iteration are returned at least once; don’t rely on it for an exact, duplicate-free snapshot if the keyspace is being modified concurrently. - Reserve
KEYSfor local development, debugging on a small dataset, or one-off scripts against a database you know is small.
Practice Exercises
- Set three keys named
invoice:1,invoice:2, andinvoice:3, each with a string value. Write theSCANcommand (withMATCH) you’d use to find all of them without ever callingKEYS, and identify what cursor value would tell you the iteration is done. - Explain, in your own words, why a pattern like
KEYS session:*that matches only 5 keys out of 5 million is just as slow asKEYS *on the same database. What single fact aboutKEYS‘s time complexity explains this? - You need to know exactly how many keys are in your database for a monitoring dashboard that refreshes every second. Which command should you use, and why would
KEYS *combined with counting the array length be a poor choice here?
Summary
KEYS patternis O(N) in the total number of keys in the database, regardless of how many match — and because Redis is single-threaded, that entire scan blocks every other client until it finishes.SCAN cursorwalks the keyspace incrementally in small, non-blocking batches, returning a new cursor each call until it returns0, at which point iteration is complete.SCANtrades a strict, instantaneous snapshot for safety — it may return duplicates and offers only an “at least once, eventually” guarantee for keys present throughout the scan.DBSIZEis O(1) and is the right tool when all you need is a total key count.KEYSis fine for local development and small, controlled datasets — never run it against a production instance with a large keyspace.
