How to List and Find Keys in Valkey and Redis: KEYS, SCAN, and Pattern Matching in Java

Published on
July 23, 2026

To list every key in a Valkey or Redis database, use KEYS *. To do it without risking an outage, use SCAN.

127.0.0.1:6379> KEYS *
1) "user:1042"
2) "session:a7f3"
3) "cart:1042"

127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 100
1) "17"
2) 1) "user:1042"
   2) "user:1043"

Both commands answer the same question. Only one of them is safe to run against production. This guide covers when each applies, how glob patterns work, what changes in a cluster, and how to do all of it from Java.

The KEYS Command

KEYS takes a single glob-style pattern and returns every matching key name.

KEYS *              # every key in the current database
KEYS user:*         # every key beginning with "user:"
KEYS session:??     # "session:" followed by exactly two characters
KEYS product:[0-9]* # "product:" followed by a digit, then anything

The supported pattern syntax is small:

PatternMatches
*Any sequence of characters, including none
?Exactly one character
[ae]One character from the set — a or e
[^a]One character not in the set
[a-c]One character in the range
\*A literal asterisk (backslash escapes)

An empty result is not an error — you get an empty array back, which is what "no keys matched" looks like.

The pattern is matched against key names only. There is no way to filter by value, and no way to sort or paginate the output. KEYS gives you everything at once or nothing.

Why KEYS Is Dangerous in Production

Valkey and Redis execute commands on a single thread. KEYS is O(N) over the entire keyspace — it walks every key, compares it against the pattern, and builds the reply. While that happens, every other client waits.

On a keyspace of a few thousand keys this is imperceptible. On a few million, it can block the server for hundreds of milliseconds. Every request from every application instance stalls for the duration, connection pools back up, timeouts start firing, and health checks fail. The KEYS call itself succeeds, which is what makes the failure confusing to diagnose afterwards.

There is a second problem. The reply is built in memory as one array. KEYS * against a large keyspace can produce a response of hundreds of megabytes, which strains both the server's output buffer and your client.

The official Redis documentation is direct about this: KEYS is intended for debugging and special operations, not for regular application code.

That leaves a few legitimate uses:

  • Local development and debugging
  • A keyspace you know is small and will stay small
  • Ad-hoc inspection against a replica, where blocking affects nobody

For everything else, use SCAN.

SCAN: The Safe Alternative

SCAN returns keys in small batches using a cursor. Each call does a bounded amount of work and returns immediately, so the server stays responsive between iterations.

127.0.0.1:6379> SCAN 0
1) "17"                    # next cursor
2) 1) "user:1042"          # this batch
   2) "session:a7f3"

127.0.0.1:6379> SCAN 17
1) "0"                     # cursor 0 means iteration complete
2) 1) "cart:1042"

Start at cursor 0, pass the returned cursor to the next call, and stop when the server returns 0 again.

Three options shape the iteration:

  • MATCH pattern — filters using the same glob syntax as KEYS. Filtering happens after keys are fetched, so a restrictive pattern doesn't make the scan cheaper; it just returns fewer results per batch. Batches can come back empty while the iteration is still running.
  • COUNT n — a hint for how much work to do per call, defaulting to 10. It is not a limit on results. Raising it to a few hundred reduces round trips at the cost of slightly longer individual calls.
  • TYPE type — returns only keys of a given type, such as list or hash.

The equivalent for iterating inside a single collection is HSCAN for hashes, SSCAN for sets, and ZSCAN for sorted sets. They follow the same cursor protocol.

The Guarantees, and the One That Catches People Out

SCAN gives you a weaker consistency model than KEYS, and it is worth knowing exactly what you are promised:

  • A key present in the database for the full duration of the iteration will be returned.
  • A key absent for the full duration will not be returned.
  • A key added or removed partway through may or may not be returned.
  • A key may be returned more than once.

That last point is the one that causes bugs. Deduplication is the caller's responsibility. If you are counting keys, or building a list to act on, collect results into a Set rather than a List.

For quick command-line work, redis-cli wraps the whole loop for you:

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

KEYS vs SCAN

KEYSSCAN
BlockingBlocks the server for the whole operationBounded work per call
ComplexityO(N) in one shotO(1) per call, O(N) total
ResultsComplete snapshot, one replyBatched across many replies
DuplicatesNonePossible — dedupe required
ConsistencyPoint-in-timeKeys changed mid-scan may be missed
MemoryEntire result set at onceOne batch at a time
Production-safeNoYes

The rule of thumb: KEYS when you're at a prompt on a small dataset, SCAN in anything that runs unattended.

Counting Keys

If you want a count rather than the key names themselves, don't scan at all:

127.0.0.1:6379> DBSIZE
(integer) 3

DBSIZE is O(1) — the server tracks the number of keys, so it answers instantly regardless of keyspace size. Use it whenever you need the total.

Counting keys matching a pattern is different: there is no built-in command, so you have to scan and count, deduplicating as you go.

Listing Keys in a Cluster

Both KEYS and SCAN operate on one node. In a clustered deployment, the keyspace is split across multiple masters by hash slot, so running KEYS * against a single node returns only that node's share.

Getting a complete picture means connecting to every master and iterating each one separately. From the command line:

redis-cli --cluster call 127.0.0.1:7000 KEYS '*'

This works, but it inherits every KEYS problem and multiplies it — you are now blocking every node in the cluster simultaneously. It is a debugging tool, not an operational one.

Doing this properly means discovering the current set of masters, opening a connection to each, running a cursor loop per node, merging the results, and re-discovering the topology if a failover happens mid-iteration. That is a meaningful amount of code to get right, and it is the part most guides skip. (Duplicates remain a per-node concern rather than a merge one — masters own disjoint hash slots, so the same key never arrives from two nodes.)

Listing Keys From Java

A low-level client gives you the commands and leaves the orchestration to you. With Jedis or Lettuce, iterating a keyspace means writing the cursor loop by hand: initialise the cursor, call scan, accumulate results into a set, check whether the returned cursor is "0", and repeat. Add cluster support and you are also managing per-node connections and topology changes.

The loop is not difficult, but it is easy to write subtly wrong — forgetting to deduplicate, treating COUNT as a hard limit, or assuming an empty batch means the iteration has finished. And because KEYS is a single call while SCAN is a loop, the dangerous option is always the more convenient one to reach for.

Listing Keys With Redisson

Redisson exposes keyspace operations through the RKeys interface, obtained from the client:

RedissonClient redisson = Redisson.create(config);
RKeys keys = redisson.getKeys();

The important design decision: getKeys() iterates using SCAN, not KEYS. No iteration method on RKeys issues a raw KEYS command, so the safe path is what you get by default rather than something you have to remember to choose. (One exception, covered below: the pattern-delete methods do fall back to KEYS inside a Lua script when called within a batch.)

// Lazily iterates the whole keyspace via SCAN
for (String key : keys.getKeys()) {
    System.out.println(key);
}

The iteration is lazy — keys are fetched in batches as you consume them, so the full keyspace is never materialised in memory.

One caveat carries over: Redisson does not deduplicate. getKeys() chains the underlying SCAN batches straight through, so the duplicate-results guarantee discussed above applies exactly as it does at the command line. Redisson removes the cursor bookkeeping and the per-node fan-out; it does not change SCAN's semantics. If you are counting or acting on each key, collect into a Set:

Set<String> unique = new HashSet<>();
keys.getKeys(KeysScanOptions.defaults().pattern("user:*"))
    .forEach(unique::add);

Narrowing the Scan With KeysScanOptions

KeysScanOptions controls pattern, batch size, result limit, and key type:

Iterable<String> found = keys.getKeys(
    KeysScanOptions.defaults()
        .pattern("user:*")     // glob pattern, same syntax as MATCH
        .chunkSize(200)        // keys loaded per request to the server
        .limit(1000)           // total keys returned, then iteration stops
        .type(RType.MAP));     // only hash-typed keys

Two of these are worth dwelling on.

chunkSize maps to COUNT, and is unset by default. Redisson only sends COUNT when you give it a positive value, so defaults() leaves the server to apply its own default of 10 keys per call. That is conservative — a scan over a large keyspace at 10 keys per round trip spends most of its time waiting on network latency. Raising it to a few hundred is usually a straightforward win.

limit is a genuine cap, unlike the COUNT hint. If you only need a sample of matching keys, limit(100) stops the iteration after 100 rather than walking the entire keyspace. In a cluster the cap is global rather than per-node — the count is tracked across the merged iteration, so limit(100) returns 100 keys in total, not 100 from each master.

The type filter uses Redisson's RType enum, which names types by their Java-side concept rather than the Redis wire name — RType.MAP filters for hashes and RType.OBJECT filters for strings:

RTypeUnderlying type
OBJECTstring
MAPhash
LISTlist
SETset
ZSETzset
STREAMstream
JSONReJSON-RL

Streams

For pipeline-style processing, getKeysStream() returns a Java Stream over the same lazy SCAN iteration:

// Audit: session keys created without a TTL, which will never expire
Set<String> neverExpires = keys.getKeysStream(
        KeysScanOptions.defaults().pattern("session:*"))
    .filter(k -> keys.remainTimeToLive(k) == -1)
    .collect(Collectors.toSet());

getKeysStream() wraps the same iterator as getKeys(), so collect to a Set here for the same reason — a List would preserve any duplicates the scan returned.

Note also that the filter issues one round trip per key. That is fine for a maintenance audit over a narrow pattern, but it is not something to put in a request path; batch the TTL lookups if the matched set is large.

remainTimeToLive returns the remaining life in milliseconds, or -1 if the key exists with no expiry set, or -2 if it doesn't exist. Test for -1 specifically rather than any negative value — lumping the two together conflates "this key will never expire" with "this key is already gone", which are different problems.

Counting and Inspecting

long total = keys.count();                           // DBSIZE — O(1)
long present = keys.countExists("user:1", "user:2"); // how many exist
String sample = keys.randomKey();                    // one key at random
RType type = keys.getType("user:1042");              // type of a given key
int slot = keys.getSlot("user:1042");                // cluster hash slot

count() is worth a closer look in a clustered deployment. It issues DBSIZE to every master in parallel and sums the results, so you get the cluster-wide total rather than one shard's share — still O(1) per node, and still far cheaper than scanning.

Cluster Behaviour

RKeys operates across the whole keyspace rather than a single node. Iterating with getKeys() against a cluster walks every master and merges the results, with topology changes handled by the client — no per-node connection management in your code.

Deleting Keys by Pattern

Once you can find keys, removing a subset of them is the natural next step. Two commands do the work:

  • DEL removes keys synchronously. For large collections it blocks proportionally to the number of elements.
  • UNLINK removes the key from the keyspace immediately and reclaims memory on a background thread. For anything large, prefer it.

Redisson exposes both, including pattern-based variants that handle the scan-and-delete loop for you:

long removed  = keys.delete("user:1042", "cart:1042");
long unlinked = keys.unlink("session:a7f3");

long byPattern    = keys.deleteByPattern("session:*");
long byPatternFast = keys.unlinkByPattern("temp:*");

Two things to know about the pattern variants.

They are not atomic in cluster mode. The implementation iterates each master separately with SCAN, buffering matches and deleting them in batches of 500, so there is no single instant at which every match disappears. Treat them as best-effort bulk operations; if you need atomicity, scope the pattern to one hash slot using hash tags.

They also behave differently inside an RBatch. Batched, the pattern variants switch to a server-side Lua script that calls KEYS internally — reintroducing exactly the blocking behaviour this article has been steering you away from. In cluster mode a batched call doesn't run at all; it throws IllegalStateException telling you to execute it as a non-batch method. Call deleteByPattern and unlinkByPattern outside a batch and you get the safe SCAN-based path.

To clear an entire database rather than a matching subset, see our guide to FLUSHALL and FLUSHDB.

Non-Blocking Variants

The same iteration is available through Redisson's Reactive and RxJava APIs, returning a stream you can compose with backpressure:

// Project Reactor
Flux<String> keyFlux = redisson.reactive().getKeys()
    .getKeys(KeysScanOptions.defaults().pattern("user:*"));

// RxJava 3
Flowable<String> keyFlow = redisson.rxJava().getKeys()
    .getKeys(KeysScanOptions.defaults().pattern("user:*"));

The async API handles iteration differently from the rest of RKeysAsync. Operations that return a single result — countAsync(), deleteAsync(), deleteByPatternAsync() — hand back an RFuture. Iteration can't, because there is no single result to complete with, so getKeysAsync() returns an AsyncIterator instead:

AsyncIterator<String> it = redisson.getKeys()
    .getKeysAsync(KeysScanOptions.defaults().pattern("user:*"));

CompletionStage<Void> drain(AsyncIterator<String> it) {
    return it.hasNext().thenCompose(hasNext -> {
        if (!hasNext) {
            return CompletableFuture.completedFuture(null);
        }
        return it.next()
                 .thenAccept(System.out::println)
                 .thenCompose(ignored -> drain(it));
    });
}

hasNext() returns CompletionStage<Boolean> and next() returns CompletionStage<String>, so the cursor advances without ever blocking a thread. Two things to watch: each invocation returns a fresh CompletionStage, so don't cache and reuse them; and because the drain is recursive, a very large keyspace can build a long completion chain — batch the results or bound the iteration with limit() if that's a concern.

See our guide to non-blocking Valkey and Redis in Java for how these fit into a wider reactive stack.

Production Checklist

  1. Never run KEYS against production. Use SCAN, or run it on a replica if you must.
  2. Always deduplicate SCAN results. Collect into a Set, not a List.
  3. Don't treat COUNT as a limit. It's a work hint; use a real limit if you need one.
  4. Tune your batch size. Redisson leaves COUNT unset by default, so the server's conservative default of 10 applies — raise chunkSize for large keyspaces.
  5. Prefer UNLINK over DEL when removing large keys or many at once.
  6. Pattern deletes aren't atomic in a cluster — and inside an RBatch they fall back to KEYS via Lua. Call them outside a batch.
  7. Design key names for scannability. A consistent type:id convention makes patterns precise and cheap to filter.
  8. If you scan on every request, you need an index. Scanning is for maintenance and debugging. For query workloads, use a secondary index — a set or sorted set of IDs — or Redis Search.

That last point is the one worth internalising. Reaching for SCAN in a hot path is usually a sign that the data model is missing an access pattern. Storing an explicit index alongside your data turns an O(N) keyspace walk into an O(1) lookup.

FAQ

How Do I Get All Keys in Redis?

Run KEYS * for a complete list, or iterate with SCAN 0 for a production-safe equivalent. In Java with Redisson, redisson.getKeys().getKeys() iterates the keyspace using SCAN.

Is the KEYS Command Safe in Production?

No. It blocks the single-threaded server for the duration of a full keyspace walk, stalling every other client. Use SCAN instead.

What Is the Difference Between KEYS and SCAN?

KEYS returns everything in one blocking call. SCAN returns small batches via a cursor, keeping the server responsive — at the cost of possible duplicate results and weaker consistency for keys changed mid-iteration.

How Do I Count the Keys in Redis?

DBSIZE returns the total in O(1). For a pattern-matched count you must scan and count, deduplicating results. In Redisson, keys.count() maps to DBSIZE.

How Do I List Keys Across a Redis Cluster?

KEYS and SCAN only cover the node you're connected to, so you must iterate every master and merge results. Redisson's RKeys.getKeys() handles this automatically.

How Do I Find and Delete Keys Matching a Pattern?

Scan for matches and delete them in batches. In Redisson, deleteByPattern() and unlinkByPattern() do this in one call — though not atomically in cluster mode.