Understanding Redis Keyspace: Deletion and Inspection Commands

Unlock the power of Redis keyspace management with this comprehensive guide. Learn to safely inspect your data using `SCAN` (and why to avoid `KEYS` in production) and efficiently delete keys with `DEL` and the non-blocking `UNLINK`. Understand the destructive nature of `FLUSHDB` and `FLUSHALL` and discover best practices for maintaining a healthy, high-performing Redis instance.

Understanding Redis Keyspace: Deletion and Inspection Commands

The Redis keyspace is just the set of keys in the currently selected database, but the way you inspect and delete those keys can decide whether a cleanup is boring or whether your application stalls in the middle of traffic.

Most teams learn this the hard way. Someone needs to delete session:* keys from a staging cache, runs KEYS session:*, sees the list, and then tries the same habit in production. On a tiny database, that feels fine. On a busy instance with millions of keys, the command can hold the server long enough for unrelated requests to queue behind it. Redis processes commands very quickly, but a command that walks the whole keyspace still has to do the work.

For day-to-day operations, think in two separate steps: find keys safely, then delete them safely. Do not treat key inspection as a harmless read. A read command can still be expensive.

Start with the database you are really using

Before deleting anything, confirm the endpoint and logical database.

redis-cli -h redis.example.internal -p 6379 INFO keyspace

The output looks like this:

# Keyspace
db0:keys=154233,expires=129900,avg_ttl=2851412
db2:keys=32,expires=32,avg_ttl=60000

That tells you which logical databases contain keys. Many Redis deployments only use database 0. Redis Cluster supports database 0 only, so FLUSHDB and FLUSHALL deserve extra care there because the usual "selected database" mental model is not useful in the same way.

If your application uses numbered databases on standalone Redis, select the right one explicitly:

redis-cli -n 2 DBSIZE

DBSIZE returns the number of keys in the selected database. It does not show names, and it does not replace an inspection pass, but it is a good sanity check before and after cleanup.

Use KEYS only when the keyspace is small and disposable

KEYS pattern returns every key matching a glob-style pattern:

KEYS user:*
KEYS cache:product:???
KEYS *

The pattern rules are handy: * matches any sequence, ? matches one character, and bracket ranges such as [0-9] match one character from the range.

The problem is not correctness. The problem is that KEYS scans the entire keyspace in one command. On a local development Redis with a few hundred keys, that is convenient. On a shared cache, it can add latency for every other client while Redis is busy producing the result. The command is documented as a keyspace command with linear complexity, so it should not be part of normal production cleanup scripts.

I still use KEYS in two places:

  • A throwaway local Redis while writing tests.
  • A small staging database where I have already checked DBSIZE and know the command cannot surprise me.

Everywhere else, use SCAN.

Use SCAN for production inspection

SCAN is cursor based:

SCAN 0 MATCH user:* COUNT 100

Redis returns two things: the next cursor and a batch of keys. Start at cursor 0. Keep scanning with the returned cursor. Stop when Redis returns cursor 0 again.

1) "24576"
2) 1) "user:100"
   2) "user:101"

COUNT is a hint, not a promise. Redis may return more keys, fewer keys, or even no keys for a given iteration. MATCH filters what comes back, but Redis still advances through the keyspace. A narrow pattern is useful for reducing client-side work, but it does not magically make every scan free.

For shell work, prefer redis-cli --scan because it hides the cursor loop:

redis-cli --scan --pattern 'session:*'

To count matching keys without printing all of them:

redis-cli --scan --pattern 'session:*' | wc -l

To inspect types before deleting:

redis-cli --scan --pattern 'session:*' | head
redis-cli TYPE session:abc123
redis-cli TTL session:abc123
redis-cli MEMORY USAGE session:abc123

TTL is especially useful for cleanup decisions. If a cache namespace already has reasonable expirations, you may not need a bulk deletion at all. Letting keys expire naturally is usually less risky than forcing a big delete during business hours.

Delete known keys with DEL

DEL removes one or more keys and returns how many existed:

DEL session:abc123
DEL session:abc123 session:def456 session:ghi789

For small keys, DEL is usually fine. Deleting a string key or a small hash is not the scary case. The case that hurts is deleting a large aggregate value, such as a list with a huge number of elements, a set used as an index, or a hash that grew far beyond its original purpose. Redis removes the key from the keyspace, but freeing a large value can cost time on the main path.

If you are deleting one key you know is small, use DEL. If you are deleting many keys or you are not sure how large they are, use UNLINK.

Prefer UNLINK for large or bulk deletion

UNLINK has the same shape as DEL:

UNLINK session:abc123
UNLINK cache:old:1 cache:old:2 cache:old:3

The important difference is memory reclamation. UNLINK removes keys from the keyspace immediately, then frees the memory asynchronously. That makes it a safer default when you are cleaning up keys that might hold large values.

This does not mean UNLINK is magic. You can still create pressure if your script discovers and unlinks millions of keys as fast as possible. Redis still has to process the commands, replicas still need to receive the changes, and memory still has to be reclaimed. Throttle bulk cleanup so you can watch latency and memory while it runs.

A practical cleanup loop looks like this:

redis-cli --scan --pattern 'session:*' |
  xargs -r -L 100 redis-cli UNLINK

That deletes in batches of 100 keys. Adjust the batch size for your environment. On a quiet maintenance window you might use larger batches. On a hot cache shared by user-facing traffic, smaller batches plus a short sleep between batches may be kinder:

redis-cli --scan --pattern 'session:*' |
while read -r key; do
  redis-cli UNLINK "$key" >/dev/null
  sleep 0.005
done

That version is slower, but it is easy to stop and easy to reason about.

Be careful with pattern deletes

Redis intentionally does not provide DEL user:*. You have to combine scanning and deletion yourself. That friction is useful because pattern deletion is where accidents happen.

Before deleting:

redis-cli --scan --pattern 'user:*' | head -50
redis-cli --scan --pattern 'user:*' | wc -l

Look at the first sample. Count the target size. If your expected cleanup was "a few thousand abandoned sessions" and the count is "most of the database," stop and fix the pattern.

Use naming conventions that make cleanup boring:

app:prod:session:<id>
app:prod:rate-limit:<user-id>
app:prod:cache:product:<id>

That is more verbose than session:<id>, but it lets you target a namespace precisely. In Redis Cluster, key names may also include hash tags such as cart:{user123}:items for slot placement. Be aware of those conventions before writing broad patterns.

FLUSHDB and FLUSHALL are reset buttons

FLUSHDB removes all keys from the selected database:

FLUSHDB
FLUSHDB ASYNC

FLUSHALL removes all keys from all logical databases:

FLUSHALL
FLUSHALL ASYNC

Modern Redis supports ASYNC and SYNC modifiers for flush commands. ASYNC schedules freeing in the background, which helps avoid a large synchronous memory-freeing pause. It does not make the operation reversible. Once the keys are gone from the keyspace, your application sees them as gone.

Before using either command, I want three checks:

redis-cli ROLE
redis-cli INFO keyspace
redis-cli CONFIG GET dir

ROLE helps confirm whether you are connected to a primary or replica. INFO keyspace shows what will be affected. CONFIG GET dir often gives another hint about which instance you are on, because data directories tend to include environment-specific paths.

For development reset scripts, be explicit:

redis-cli -h 127.0.0.1 -p 6379 -n 0 FLUSHDB ASYNC

Avoid scripts that run redis-cli FLUSHALL with defaults. Defaults change when a script runs on another host, in another container, or from a CI runner with different environment variables.

Verify after deletion

After a cleanup, check both count and application behavior:

redis-cli --scan --pattern 'session:*' | wc -l
redis-cli INFO memory
redis-cli INFO stats | grep expired_keys

Memory may not fall instantly after UNLINK or asynchronous flush because freeing happens in the background and allocators may keep memory reserved for reuse. That is not automatically a leak. Watch used_memory, latency, and whether the key count moves in the direction you expect.

For production changes, write down the exact command and pattern before running it. A safe Redis cleanup is not just the right command. It is the right command, against the right instance, with a pattern you have sampled, during a window where you can watch the result.

A safer production cleanup runbook

For a real cleanup, I like to turn the command into a small runbook instead of a one-liner typed from memory. The runbook does not need to be fancy. It should answer four questions: what instance, what pattern, how many keys, and how fast.

Start with read-only checks:

redis-cli -h redis.example.internal -p 6379 ROLE
redis-cli -h redis.example.internal -p 6379 INFO keyspace
redis-cli -h redis.example.internal -p 6379 --scan --pattern 'app:prod:session:*' | head -20
redis-cli -h redis.example.internal -p 6379 --scan --pattern 'app:prod:session:*' | wc -l

Then inspect a few representative keys:

redis-cli TYPE app:prod:session:sample
redis-cli TTL app:prod:session:sample
redis-cli MEMORY USAGE app:prod:session:sample

If the sample shows keys that should naturally expire in a few minutes, wait instead of deleting. If the keys have no TTL and clearly belong to abandoned data, proceed with a throttled cleanup. Keep a terminal open with latency or memory:

redis-cli --latency
redis-cli INFO memory

For shared production Redis, I prefer a script that can be stopped without losing track of intent:

redis-cli --scan --pattern 'app:prod:session:*' |
while read -r key; do
  redis-cli UNLINK "$key" >/dev/null
  sleep 0.002
done

That is not the fastest version. It is the version that gives you a chance to notice if latency moves, clients complain, or the pattern was broader than expected. When the instance is quiet and the count is modest, batching with xargs -L 100 is fine. The point is to choose the pace intentionally.

One more habit helps: save the count before and after in the incident ticket or deployment note. "Deleted session keys" is not enough. "Deleted 48,213 keys matching app:prod:session:* from db0 on redis-cache-01 using UNLINK, no latency increase observed" is the kind of note that saves time later.