Coreframe Labs

Automated performance audit

Performance audits for Redis caching

A cache that is wrong is worse than no cache. We run a two-tier hot/warm layer that holds a 94% hit rate in production; getting there is mostly about key design and expiry behaviour, which is exactly what these four patterns get wrong.

Check a file now — free

Paste one file. The free tier runs a single security pass and returns up to three findings, each with the line of code that evidences it. Nothing is stored, nothing is used for training, and detected secrets are redacted before analysis.

Paste one file0 / 400 lines

Not stored. Not trained on. Secrets redacted before analysis.

What we find most often in Redis caching

Each of these is a real pattern, with the code that causes it and the change that fixes it.

1. Cache key missing the tenant

Symptom
Users occasionally see another organisation's data. Never reproducible locally.

The pattern

const key = `dashboard:summary`;
const cached = await redis.get(key);

Why it bites
One key serves every tenant, so whichever request populates it first shares its data with everyone until expiry. Single-tenant development and staging never surface it — it needs concurrent multi-tenant traffic to appear.

The fix

// Every variable that changes the value belongs in the key.
const key = `dashboard:summary:v2:${orgId}:${period}`;

2. Thundering herd on expiry

Symptom
A latency and CPU spike at a fixed interval, exactly matching the TTL.

The pattern

let data = await redis.get(key);
if (!data) {
  data = await expensiveQuery();          // every concurrent request runs this
  await redis.setex(key, 300, JSON.stringify(data));
}

Why it bites
When the key expires, every in-flight request misses simultaneously and all of them run the expensive query against the database at once. The cache amplifies the spike instead of absorbing it.

The fix

let data = await redis.get(key);
if (!data) {
  // First caller wins the lock and refreshes; the rest wait briefly and re-read.
  const gotLock = await redis.set(`${key}:lock`, "1", "EX", 10, "NX");
  if (gotLock) {
    data = await expensiveQuery();
    // Jitter the TTL so keys don't re-expire in lockstep.
    await redis.setex(key, 300 + Math.floor(Math.random() * 60), JSON.stringify(data));
    await redis.del(`${key}:lock`);
  } else {
    await new Promise((r) => setTimeout(r, 50));
    data = await redis.get(key);
  }
}

3. KEYS against a production instance

Symptom
Periodic full-server stalls; every client times out at once.

The pattern

const stale = await redis.keys("session:*");

Why it bites
Redis is single-threaded and `KEYS` walks the entire keyspace before returning. On a few million keys that blocks every other command for seconds — the whole server, not just the caller.

The fix

// SCAN is incremental and cooperative.
let cursor = "0";
do {
  const [next, batch] = await redis.scan(cursor, "MATCH", "session:*", "COUNT", 100);
  cursor = next;
  if (batch.length) await redis.unlink(...batch);   // UNLINK frees asynchronously
} while (cursor !== "0");

4. Cached entries with no TTL

Symptom
Memory climbs steadily until Redis starts evicting keys you needed.

The pattern

await redis.set(`user:${id}:profile`, JSON.stringify(profile));

Why it bites
A `SET` with no expiry keeps the key forever. Memory grows with total users ever seen rather than active users, and once `maxmemory` is reached the eviction policy — not you — decides what to drop.

The fix

await redis.setex(`user:${id}:profile`, 3600, JSON.stringify(profile));
// And set an explicit policy so eviction is a decision, not a surprise:
//   maxmemory-policy allkeys-lru

An automated pass reads one file. An audit reads the system.

The tool above is deliberately narrow — one file, one dimension, one pass. The issues that actually take systems down live between files: in the trust boundary nobody documented, the query pattern that only emerges at scale, the cache that was correct until the second tenant arrived. That is what the Coreframe Technical Audit is for — five business days, a written report, a live findings call, and a remediation proposal you can execute with or without us.

£1,500–£2,500 solo · £3,000–£5,000 funded startup · £3,000–£6,000 SME

See what the full audit covers →

Related audits

All stack audits →