As33
@periodic/
arsenic
redis_keys
🔴 Critical

Full keyspace scan — blocks all Redis operations while running

The KEYS command performs a full scan of the entire Redis keyspace. It is a single-threaded O(N) operation — while it runs, every other Redis command waits. On a database with millions of keys this can stall Redis for seconds, causing cascading timeouts across all clients sharing the instance.

Common Causes

  • Debugging sessions left in production code
  • Admin scripts using KEYS to enumerate or clean up keys
  • Pattern-matching deletions using KEYS + DEL in a loop
  • Cache invalidation logic that scans for prefix-matched keys

How to Fix

  1. 1.Replace KEYS with SCAN and a cursor — it iterates in small batches without blocking
  2. 2.Maintain a secondary index (e.g. a Redis Set) to track related keys by prefix
  3. 3.Use Redis key namespacing and maintain explicit lists of keys per namespace
  4. 4.Restrict KEYS via Redis ACLs so it cannot fire in production at all

Never use KEYS in production

KEYS is documented by Redis itself as not suitable for production use. There is no safe "small dataset" exception — even a brief KEYS call on a shared instance will block every other client for its entire duration.

Example

typescript
// BAD — blocks the entire Redis instance
const keys = await redis.keys('session:*');
for (const key of keys) {
  await redis.del(key);
}

// GOOD — cursor-based scan, non-blocking
let cursor = '0';
do {
  const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', 'session:*', 'COUNT', 100);
  cursor = nextCursor;
  if (keys.length > 0) {
    await redis.del(...keys);
  }
} while (cursor !== '0');

// GOOD — maintain an explicit Set of keys
// When you write: redis.set(`session:${id}`, data)
// Also track: redis.sadd('sessions:index', `session:${id}`)
// To enumerate: redis.smembers('sessions:index')

Why SCAN is safe and KEYS is not

SCAN uses a cursor to return a small batch of keys per call. It shares processing time with other commands — each call releases the server after its batch. The full scan may take many round trips, but no single call monopolises the instance.

typescript
// Helper: collect all matching keys without blocking
async function scanKeys(redis: Redis, pattern: string): Promise<string[]> {
  const result: string[] = [];
  let cursor = '0';

  do {
    const [next, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200);
    cursor = next;
    result.push(...keys);
  } while (cursor !== '0');

  return result;
}

const sessionKeys = await scanKeys(redis, 'session:*');

Restricting via ACLs

bash
# In redis.conf — deny KEYS to the application user
ACL SETUSER appuser ~* +@all -keys -flushall -flushdb

# Verify
redis-cli ACL WHOAMI
redis-cli ACL LIST