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), or user:[1-3]000 (character ranges).
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
  • cursor — start iteration with 0; on each call, pass back the cursor value Redis just returned to you, and stop once it returns 0 again.
  • 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. string or hash.

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, 1003KEYS 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

  1. The client sends KEYS pattern (or, internally, one call in a SCAN loop).
  2. Redis’s single command-processing thread begins iterating the main keyspace hash table bucket by bucket.
  3. 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. For SCAN: only a bounded number of buckets (per COUNT) are visited before the call returns, so the thread is free to interleave other clients’ commands between successive SCAN calls.
  4. Matching key names are collected into a reply array and sent back to the client — for KEYS, all in one round trip; for SCAN, one small batch plus a cursor per round trip.
  5. The client repeats the process (only relevant for SCAN) by sending the returned cursor back in, until Redis returns a cursor of 0, 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 KEYS against a production instance with a nontrivial dataset — use SCAN, or run redis-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 a KEYS result.
  • 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, keep COUNT modest (tens to low hundreds) so each call stays cheap, and always loop until the cursor returns to 0 — 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 KEYS for 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, and invoice:3, each with a string value. Write the SCAN command (with MATCH) you’d use to find all of them without ever calling KEYS, 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 as KEYS * on the same database. What single fact about KEYS‘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 pattern is 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 cursor walks the keyspace incrementally in small, non-blocking batches, returning a new cursor each call until it returns 0, at which point iteration is complete.
  • SCAN trades 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.
  • DBSIZE is O(1) and is the right tool when all you need is a total key count.
  • KEYS is fine for local development and small, controlled datasets — never run it against a production instance with a large keyspace.