Redis · intermediate
Redis cache-key drift: find readers and writers using different contracts
When writers and readers of a Redis cache derive different keys for the same logical resource, reads silently miss or hit stale entries. The classic cache-aside pattern only works if every path into the cache agrees on the key contract; even a small serializer change, prefix change, or whitespace difference can split the namespace and make the cache look like a flaky datastore. This guide shows how to evidence cache-key drift from Redis itself, find every reader and writer, and decide whether to realign the code or to introduce a versioned key prefix.
The symptoms
- •Repeated Redis MISS for a resource that was clearly written moments earlier, visible as a hit rate near zero for a specific key family while siblings stay healthy.
- •Latency spikes correlate with cache MISS events for warm logical resources, with origin fetches re-populating the cache in the same request path.
- •Two application instances serve different values for the same id, but only after a deployment or library change; values stabilize per-instance but disagree across instances.
- •Sudden growth in a key family that has no matching reads, or reads that return payload-sized blobs decodable into scalar types where objects are expected.
- •Bloom-filter or guard checks on the read side disagree with the condition the writer used, suggesting the writer computed a different identity than the reader checked.
- •Keys observed in Redis differ from the keys your instrumentation logs, and the divergence starts at a specific commit or rollout boundary.
Likely causes
- •Independent key-construction helpers in different modules, each with its own serialization for ids, scopes, or version tags.
- •A serializer change that alters the canonical form of composite keys, such as adding locale, tenant scope, or hash algorithm, applied to writers but not all readers.
- •Hand-edited key strings in one path versus a shared helper in another, often introduced during a hotfix that bypassed the helper.
- •Prefix drift when a service-name, tenant-id, or region variable differs between writer and reader due to environment configuration.
- •Hashing inconsistency where one side hashes a normalized string and another hashes the raw user input, producing different keys for the same logical resource.
- •Key rotation or namespace migration that was rolled out to some callers but not others, leaving two parallel key families alive.
First ten minutes
- 01Capture the exact key the writer stored and the exact key the reader looked up for the same logical id; do not rely on application logs alone.
- 02Run an in-Redis SCAN over the suspected key family and list the actual key strings so you can compare them to your helper output.
- 03Identify each caller that performs GET, SET, DEL, and EXISTS for the family and tag it with its source file, deploy, and config.
- 04Inspect Redis SLOWLOG or METRICS for the family's hit ratio and TTL distribution to confirm reads are missing, not failing.
- 05Compare the two key strings byte by byte, including any case, separator, or trailing-slash differences, before assuming a logical bug.
- 06Quarantine the suspected family by moving tests to a separate key prefix and stop further writes until the contract is confirmed.
Evidence to collect
- •Side-by-side capture of the writer's key string and the reader's key string for the same logical id, including length and a hex of the raw bytes.
- •Annotated SCAN output listing unique key prefixes and key counts per prefix inside the affected family.
- •List of every code path that calls GET, SET, DEL, SETEX, or EXISTS for the family, with file, function, and config it reads.
- •Hit ratio, miss ratio, and TTL histogram for the affected family from Redis INFO stats or an external metrics source.
- •Deployment and configuration diff between the last known-good and the first observed drift, narrowed to the key-construction area.
- •Two distinct responses from two instances for the same id, kept as a single artifact with timestamps and request inputs.
Where to look
- •Inside Redis, at the boundary between SCAN, INFO keyspace, and TTL output, where the actual keyspace truth lives apart from any application log.
- •At the helper-versus-callsite boundary in the codebase, where one module hands a key string to another and the contract can be dropped.
- •At the configuration boundary between services, where prefix variables are read from environment, config files, or feature flags.
- •At the serialization boundary inside the cache layer, where ids, scopes, and version tags are concatenated or hashed before storage.
- •At the deploy and rollback boundary, where multiple versions of the same binary run concurrently and can disagree on the helper.
- •At the migration boundary, where a previous key family was renamed and old keys may still be served by some readers.
Diagnostic steps
- 01Use SCAN with the family's pattern and group the resulting keys by their prefix and tail to expose silent duplicates that survive a naive helper.
- 02Compose the writer key and the reader key for the same id in a controlled test and diff them bytewise, including any case and separator, before concluding the code is wrong.
- 03Trace every callsite that constructs a key for the family and list each at a single table with its source file, function, and the helpers it uses.
- 04Compare the helper outputs against the actual SCAN output to see whether the visible keys match what the helper would produce, which is the load-bearing evidence for drift.
- 05Cross-check the metric hit ratio for the family against the application's logged miss rate; if Redis shows hits but the app reports misses, the read key is wrong.
- 06Reproduce a single read with command-line tooling to confirm the failure with a fixed input, then trace the values through the helper to localize the divergence.
- 07Decide between realigning the helper for all callers and introducing a versioned key prefix based on whether the old key family must remain readable during migration.
Common mistakes
- •Reading from cache and never noticing the miss because the origin serves a fresh value, so the bug presents as latency rather than correctness.
- •Trusting application-side keys without verifying against SCAN, which lets typo or empty-string prefixes hide in the helper layer.
- •Assuming a library is uniform when two versions of the same library are loaded, each producing a different key shape.
- •Fixing one callsite and missing the others because the helper itself is the source of truth, leaving drift to recur in the next caller.
- •Rotating a key prefix without a dual-read strategy, so readers that still send the old key family miss for the entire migration window.
- •Confusing encoding differences with drift, for example comparing a UTF-8 string to a JSON-escaped string that represents the same resource.
Safe fixes
- •Centralize all key construction for the family in a single helper and route every callsite through it, then re-run the SCAN comparison to confirm parity.
- •If a config-driven prefix is the source of drift, document the exact constructor at the boundary and add a startup self-check that fails fast when the prefix is empty.
- •When the serializer changed, freeze the helper at the old behavior for existing families and add a new helper version for new families, with parallel reads until the old family ages out.
- •Use a versioned prefix such as a single character schema tag so a future change can be rolled out without invalidating the entire cache at deploy time.
- •Add a read-side fallback that tries the alternative key under a feature flag, then compare values to confirm the contract before turning the fallback into the primary path.
- •Wrap the helper with a canary that emits both the new and old key for a sample of traffic, then validate equality before the wider rollout.
Prove the fix
- 01For a fixed input id, the writer's stored key and the reader's lookup key are byte-equal when inspected through SCAN and a manual probe, with no parallel key families outside the versioned prefix.
- 02Hit ratio for the family returns to the pre-incident level within a configurable window after the helper change is fully rolled out, with no new key prefixes appearing in SCAN.
- 03Two application instances served the same id at the same time and returned identical values, with the divergence timestamp logged and absent after the change.
- 04Application-side miss rate for the family matches the Redis miss rate within noise, so writers and readers are observing the same keyspace.
- 05A regression test exists that runs the writer and reader helpers against the same id and asserts byte-equal output, and the test is wired into the deployment pipeline.
- 06No untouched keys in the legacy family remain after the agreed TTL window, confirmed by a final SCAN that returns zero matches for the old pattern.
Prevention and next steps
- •Make the key constructor a single function with a documented contract, and forbid string concatenation outside it for any cache key.
- •Add a contract test that asserts equality between the writer key and the reader key for a fixture of inputs, including ids with special characters and empty components.
- •Treat any change to the constructor as a migration that requires a dual-read strategy and a documented grace period for the old key family.
- •Expose a key-level metric so drift causes a measurable regression instead of a silent miss, and alert on sudden growth in unprefixed or unexpected key families.
- •Version the key prefix visibly so a future change can be introduced without conflating old and new keys in the same keyspace.
Safe commands and checks
redis-cli -h <host> -p <port> --scan --pattern "<family-prefix>:*" | head redis-cli -h <host> -p <port> GET "<writer-key-for-id>" redis-cli -h <host> -p <port> GET "<reader-key-for-id>" redis-cli -h <host> -p <port> INFO keyspace redis-cli -h <host> -p <port> INFO stats | grep -E "keyspace_hit|keyspace_miss" redis-cli -h <host> -p <port> TTL "<writer-key-for-id>" redis-cli -h <host> -p <port> OBJECT ENCODING "<writer-key-for-id>"