Dev iconDevSep 15, 2026 ~7 min source read

Ten Million Keys, One Missing Index

A developer-built demo shows how adding a per-entity index to Redis cache keys replaces costly full-keyspace scans (KEYS) with short, targeted lookups and deletions. The article explains the problem, the key format the read path uses, an index-based invalidation strategy, and pitfalls to avoid.

Ten Million Keys, One Missing Index

Share this story

Send the public story page.

Useful takeaways from this story.

DEV Community Open original cover image This article explores how a per-entity index improves Redis cache invalidation by replacing repeated full-keyspace scans with targeted lookups.

❗ The problem A cache often sits in front of a slower dependency: a third-party API, a busy database or a service with a rate limit.

For example, if you use the KEYS command to search for records matching an invalidation pattern across a dataset containing several million keys, the command can block the Redis thread for seconds.

# The problem

Many teams discover keys using Redis's KEYS command or SCAN. On large datasets, KEYS can block the Redis thread for seconds. Redis is single-threaded for command execution, so a blocking search stalls every other client, including simple GETs.

# How the read path builds a key

service::tenant::category::entityId::params

Where params are canonical JSON (recursively sorted keys) of the method's tail arguments. The first argument must be a string entityId and validated as a key segment. Because canonicalJson normalizes parameter order, requests with the same logical params map to the same key.

The important point: the read path has the full arguments and constructs the exact cache key strings. The delete/invalidate path normally only has an entity id and cannot reconstitute the many different keys the read path produced.

# The missing index and how it helps Instead of scanning the entire keyspace, the author introduces a per-entity index: a small Redis structure that maps an entityId to the list (or set) of full cache key strings that contain that entityId.

  • On a cache write (when a read path populates the cache after a miss), also add the generated full key to the entity's index entry.
  • On invalidation for an entity, fetch the index entry and run targeted DEL commands for the listed keys.
  • Optionally remove the index entry or prune it as keys expire.

This replaces a KEYS or pattern scan with a single index lookup plus a short batch of deletes. The index lookup is cheap and non-blocking compared with scanning millions of keys.

  • buildCacheKey enforces the contract: first argument is entityId (string), remaining args become canonical JSON params.
  • Index entries must be kept consistent with cache writes and deletes: add on write, remove on explicit delete, allow TTLs or background compaction for stale entries.

# Pitfalls and related anti-patterns

  • Do not treat origin failures as valid cache values. The article shows an anti-pattern where a caught error returns undefined and that undefined is stored as a negative cache result. That turns transient failures into incorrect cached state.
  • Index maintenance is extra work: you must handle race conditions, expirations, and potential index growth. The demo code and scripts show one approach but evaluate it against your workload.

# Where to try this The author includes an accompanying demo repository with the implementation, benchmark scripts, and recorded results. Run it locally to repeat experiments and test different workloads.

More context around this story.

Omega8.cc: Keys to the Engine Bay
Omega8 iconOmega8Sep 11, 2026

Omega8.cc: Keys to the Engine Bay

Handing an outside developer your one and only SSH login because the job was one theme on one site is the oldest mistake in hosting, and the collaborator button on the big platforms only hides the question of what that seat can actually do. On a BOA server a Client, the control panel's word for the person a site belong

Keep reading in the app

Open the app view to save this story, compare related coverage, and continue from the same source.

Open in app