← The blog
Infrastructure·May 2025·7 min read

Caching strategy for a dashboard: Redis and revalidation

Expensive aggregates cached in Redis with a short TTL, and a targeted bust on write so numbers stay current.

A dashboard that runs the same heavy aggregate query on every page load will fall over the moment a few people open it at once. Those numbers do not change second to second, so caching them in Redis with a short TTL took one panel from a 900 ms query to a 4 ms lookup. The trick is busting the cache precisely when the underlying data actually changes.

Cache the expensive shape

The candidates are the queries that are costly and tolerant of being a little stale. Monthly revenue, active user counts, and rollups across large tables fit perfectly. They take real work to compute and nobody needs them accurate to the millisecond.

async function getRevenue(tenantId: string) {
  const key = `dash:revenue:${tenantId}`;
  const hit = await redis.get(key);
  if (hit) return JSON.parse(hit);
  const data = await computeRevenue(tenantId); // the slow query
  await redis.set(key, JSON.stringify(data), "EX", 300);
  return data;
}

A five-minute TTL means the worst-case staleness is five minutes, and the database runs the heavy query at most twelve times an hour per tenant no matter how many people are looking.

TTL alone is not enough

A pure TTL means a brand new invoice does not show up for up to five minutes, which looks broken to the person who just created it. So writes that affect a cached value delete that key, and the next read recomputes it. TTL is the safety net for staleness you did not predict, the targeted bust is for the changes you did.

async function createInvoice(input: InvoiceInput) {
  const invoice = await db.invoice.create({ data: input });
  await redis.del(`dash:revenue:${input.tenantId}`);
  return invoice;
}

Key by what varies

The cache key carries every dimension the value depends on, the tenant, the metric, and the time range if there is one. Get this wrong and one tenant sees another tenant's numbers out of the cache, which is worse than no cache at all. The key is part of the correctness, not just a label.

Pair it with Next revalidation

For data rendered on the server, Next's own caching layers on top. A page can cache its render and revalidate on a tag, while Redis caches the raw aggregate underneath. A write busts the Redis key and calls revalidateTag, so both layers refresh together.

revalidateTag(`revenue-${tenantId}`);

Never cache the empty error

The bug that taught me a lesson was caching a failed query result, so a transient database hiccup served zeros for five minutes. Now the cache only ever stores a successful, validated result, and a failure falls through to recompute next time. Cache the good answer, bust it on write, key it by every variable, and never let a five-minute TTL immortalize a mistake.

RedisCachingNext.jsPerformance
Let's talk

Let's build
SOMETHING REAL.

Have a system that needs to think, scale, or just finally get shipped? Let's make it happen.

Based in Dhaka, Bangladesh · Available worldwide