# How to List Keys in Redis Safely With SCAN

> **Source:** https://upstash.com/kb/redis-keys-vs-scan
> **Date:** 2026-11-02
> **Author(s):** Josh
> **Topic:** Redis
> **Format:** text/markdown, machine-readable content for agents and LLMs

We ran KEYS and SCAN against 1 million keys on Upstash Redis and stock Redis 8. See how much SCAN helps, when KEYS stops working, what COUNT to use, and why a key prefix matters more than COUNT.

---

`KEYS user:*` works fine on a small database. We grew a real Upstash Redis database a few thousand keys at a time, and just past 90,000 keys the same call stopped working:

```text
ERR KEYS command is disabled because total number of keys is too large, please use SCAN
```

The commands were executed against an Upstash Redis Pay as You Go database and a free [scratch database](https://upstash.com/docs/devops/developer-api/start-redis/create), from a client that was 1 ms away. Stock Redis tests used a Redis server with default settings, running on the same machine as the client.

A stock Redis 8 instance running ten concurrent `KEYS` commands caused a read command from another client to take 823 ms (normal read: 0.08 ms) when there were 1 million keys. Stock Redis doesn’t reject that command; it just runs it while other clients have to wait.

## Why should you use SCAN instead of KEYS?

Use `SCAN` instead of `KEYS`. `KEYS` scans all the keyspace with a single command. On stock Redis, this blocks all clients. On Upstash, this command fails as the database grows.

The Redis docs say it plainly: [don't use `KEYS` in regular application code](https://redis.io/docs/latest/commands/keys/). This is what a second client saw on stock Redis while `KEYS` ran:

| Keys in database | Call | `KEYS` took | Slowest read by the other client |
| --- | --- | --- | --- |
| 100,000 | Narrow pattern, 10 matches | 7 ms | 6 ms |
| 1,000,000 | Narrow pattern, 100 matches | 84 ms | 82 ms |
| 1,000,000 | `KEYS *` | 690 ms | 203 ms |
| 1,000,000 | 10 narrow calls at once | 825 ms | 823 ms |

This isn’t just theoretical — our support team has seen databases that scan their whole keyspace 525 times a day, or even once a second from edge functions.

Even with a narrow pattern, scanning all those keys is still expensive, so Redis runs almost as slowly. That cost compounds, too — we end up with multiple concurrent requests running `KEYS`, which means they block on each other while also blocking everybody else.

## How KEYS fails on Upstash

Upstash Redis fails in two stages. We grew the database and tried two calls at each size:

| Keys in database | Call that matches every key | Call that matches one key |
| --- | --- | --- |
| 5,001 | returned 5,000 keys | 2.2 ms |
| 6,001 | `too many keys to fetch` | 2.3 ms |
| 90,001 | `too many keys to fetch` | 15.2 ms |
| 100,001 | `KEYS command is disabled` | `KEYS command is disabled` |

This table comes from the scratch database, which held nothing else, and the Pay as You Go database hit the same two limits. Other plans may differ, so it's a good practice to keep `KEYS` out of application code across all plans. It's also something that's not obvious on a test database with only a few thousand keys.

And also that even though the pattern in the narrow `KEYS` call only matched one key, the call itself still takes more time as the number of keys increases: from 1.7 ms for 1,000 keys to 15.2 ms for 90,000 keys. It is iterating the whole keyspace, and finally fails around 100,000 keys.

## What COUNT should you use with SCAN?

You will want to set the `COUNT` to 1000. This is the parameter [to tell Redis how much work it can do per call](https://redis.io/docs/latest/commands/scan/). The default is 10, which can make this take very long and require many round trips.

Each page is also one [billed command](https://upstash.com/docs/redis/overall/billing) on Upstash. Walking all 1 million keys with no pattern:

| `COUNT` | Pages | Time |
| --- | --- | --- |
| 100 | 10,102 | 9.9 s |
| 1000 | 1,011 | 1.7 s |
| 10000 | 1,011 | 1.7 s |

On this database, there were at most 1,000 keys per page, so going higher than 1000 had no effect.

![](https://cdn.bydefault.so/drawing-z2aoAnPB9U4KYKQWIwN0A.png)

## A key prefix matters more than COUNT on Upstash

At least on Upstash, things worked differently if the pattern had a fixed prefix. On stock Redis, this doesn’t optimize anything, because Redis [applies the pattern after it reads the keys](https://redis.io/docs/latest/commands/scan/). If the pattern is rare, there will be many empty pages.

This is the cost of finding 100 keys among 1 million:

| Engine | Pattern | `COUNT` | Pages | Empty pages | Time |
| --- | --- | --- | --- | --- | --- |
| Stock Redis | `app:rare:*` | 100 | 9,953 | 9,853 | 625 ms |
| Stock Redis | `app:rare:*` | 1000 | 1,000 | 905 | 180 ms |
| Stock Redis | `app:rare:*` | 10000 | 100 | 32 | 143 ms |
| Upstash | `app:rare:*` | 1000 | 1 | 0 | 2 ms |
| Upstash | `*:rare:*` | 1000 | 93 | 91 | 607 ms |

The stock Redis times have no network in them, because the client ran on the same machine. Over a real network, each of those pages also pays a round trip. The page count is the number to compare.

On Upstash, the same 100 keys took one page with a fixed prefix and 93 pages with a leading wildcard. That is 2 ms against 607 ms. These numbers suggest the engine can jump straight to a fixed prefix, while a leading wildcard has to walk the keys.

![](https://cdn.bydefault.so/image-Q2g3EIWmJXu-TQAPwbBFN.png)

This loop handles either case. Empty pages are normal, so only a cursor of "0" ends it:

```ts
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

async function keysByPrefix(prefix: string) {
  const found: string[] = [];
  let cursor = "0";
  do {
    const [next, keys] = await redis.scan(cursor, { match: `${prefix}*`, count: 1000 });
    found.push(...keys);
    cursor = next;
  } while (cursor !== "0");
  return found;
}

async function deleteByPrefix(prefix: string) {
  let cursor = "0";
  let deleted = 0;
  do {
    const [next, keys] = await redis.scan(cursor, { match: `${prefix}*`, count: 1000 });
    if (keys.length > 0) deleted += await redis.unlink(...keys);
    cursor = next;
  } while (cursor !== "0");
  return deleted;
}

await redis.mset(Object.fromEntries(Array.from({ length: 2500 }, (_, i) => [`tenant:42:item:${i}`, i])));

console.log((await keysByPrefix("tenant:42:")).length);
console.log(await deleteByPrefix("tenant:42:"));
console.log((await keysByPrefix("tenant:42:")).length);
```

```text
2500
2500
0
```

Also, `SCAN` [can return the same key twice](https://redis.io/docs/latest/commands/scan/). When performing any action on each key, this needs to be accounted for.

## Keep an index when you ask the same question often

This sweep technique is good for a one-off. Apps that query the same set of keys, on a schedule or per request, can store the key names in a set and retrieve them with a single command. That’s how the same 100 keys were found out of 1 million, with one `SMEMBERS` call that took 1.4 ms on Upstash.

Each write now also updates the set, and this code keeps the set from outliving its keys:

```ts
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

async function saveItem(tenant: string, id: string, value: string) {
  const key = `tenant:${tenant}:item:${id}`;
  const pipeline = redis.pipeline();
  pipeline.set(key, value, { ex: 3600 });
  pipeline.sadd(`tenant:${tenant}:items`, key);
  pipeline.expire(`tenant:${tenant}:items`, 3600);
  await pipeline.exec();
}

await saveItem("42", "a", "first");
await saveItem("42", "b", "second");

console.log((await redis.smembers("tenant:42:items")).sort());
```

```text
[ 'tenant:42:item:a', 'tenant:42:item:b' ]
```

The set can get large as well. When it gets to many thousands of keys, then read it in pages with `SSCAN` because [a reply size is capped to 10 MB](https://upstash.com/docs/redis/troubleshooting/max_request_size_exceeded) on Free and Pay as You Go.

## What to use

- Use `KEYS` only manually, for debugging, on a small database.
- Use `SCAN` with `COUNT` 1000 in application code.
- If deleting by prefix, combine `SCAN` with `UNLINK`.
- If prefixing keys, use a fixed prefix like `tenant:42:item:` to make it possible to use it in the pattern.
- If you need to list the same group of keys often, consider storing the keys in a set.

---

This site has a search endpoint: https://context7.com/api/v2/ask?siteKey=ask_4cf2adc7846aa874f833b068&query=<URL-encoded question>. It returns documentation that answers the question, with a source link for each part. No API key is needed. If nothing matches, it says so.
