Tech Stack13 June 2026 · 9 min readUpdated 21 June 2026

Caching Strategy for SaaS: Redis Patterns That Scale

A practical Redis caching playbook for SaaS: cache-aside, write-through, per-tenant key namespacing, TTL choices, and the invalidation traps that cause stale-data bugs.

Caching Strategy for SaaS: Redis Patterns That Scale

A caching strategy for SaaS starts with one decision: cache read-heavy, slow-to-compute data that tolerates being a few seconds stale, and never cache anything you cannot safely invalidate. In practice that means Redis sitting in front of your database with the cache-aside pattern, keys namespaced by tenant, and a TTL on every entry so a missed invalidation self-heals instead of serving wrong data forever. Get those three things right and caching removes most of your read load; get them wrong and you ship a stale-data bug that is hard to reproduce.

This is a supporting deep-dive for the SaaS Backend Infrastructure pillar. Caching is one of the highest-impact backend levers — but it is also where multi-tenant apps quietly leak one customer's data into another customer's screen if you skip the namespacing step.

Key takeaways

  • Cache-aside is the default. The app reads from Redis, falls back to the database on a miss, then writes the result back. It is simple, framework-agnostic, and fails safe — if Redis is down, you still serve from the database.
  • Namespace every key by tenant. In a multi-tenant SaaS, tenant:{id}:user:{uid} is non-negotiable. A shared key like user:{uid} is how you serve tenant A's cached data to tenant B.
  • Always set a TTL. A TTL is your safety net for the invalidation you forgot. No entry should live forever — stale-but-bounded beats wrong-forever.
  • Invalidation is the hard part. Decide per data type whether you delete the key on write (write-through-ish) or let the TTL expire it. Mixing the two carelessly is the classic source of "why is the old value still showing?".
  • Cache the expensive reads, not everything. Dashboards, aggregate counts, permission lookups, and config blobs benefit most. A row you read once and write often is usually not worth caching.

When does a SaaS actually need Redis caching?

Not on day one. A clean Postgres or Firestore query with the right index serves a small product fine, and adding a cache before you have a measured read-latency problem just adds a second source of truth to keep consistent. You reach for Redis when one of these is true: a query is genuinely expensive (joins, aggregates, full-text), the same data is read far more often than it changes, or you are hitting a per-call quota or cost ceiling on a downstream service.

A concrete example from my own work: when I built Callidus, a React + TypeScript + Firebase clinic SaaS, the per-tenant Firestore security rules check the tenant ID from a JWT claim on every read. Firestore handles that well, but the same shape of problem on a relational backend — where every dashboard widget re-runs an aggregate over a tenant's appointment history — is exactly where a cached aggregate earns its keep. The rule of thumb: cache the read whose cost you can name in milliseconds and whose freshness you can tolerate in seconds.

What is the cache-aside pattern and why is it the default?

Cache-aside (also called lazy loading) puts the application in control. The flow is:

  1. App asks Redis for the key.
  2. Hit → return it. Miss → query the database.
  3. Write the database result into Redis with a TTL.
  4. Return the result.
async function getTenantDashboard(tenantId) {
  const key = `tenant:${tenantId}:dashboard`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const data = await db.computeDashboard(tenantId); // expensive
  await redis.set(key, JSON.stringify(data), 'EX', 60); // 60s TTL
  return data;
}

Why it is the default: it only caches data that is actually requested (no wasted memory on cold keys), and it degrades gracefully. If Redis is unreachable, your code still hits the database — slower, but correct. The trade-offs are a cache-miss latency penalty on the first read and the small risk of a stampede when many requests miss the same hot key at once (covered below).

Cache-aside vs write-through: which write pattern?

The difference is who populates the cache on writes.

  • Cache-aside: writes go to the database only. The cache is updated lazily on the next read (after you delete the stale key). Simple, and the cache never holds data nobody asked for.
  • Write-through: every write updates the database and the cache synchronously. Reads are always warm, but you pay write latency on every update and risk caching data that is never read again.

For most SaaS, cache-aside plus explicit invalidation on write is the pragmatic middle: on a write, delete the affected keys rather than re-populating them, and let the next read repopulate. That avoids caching cold data while keeping reads correct. Reserve true write-through for a small set of always-hot keys (e.g. a feature-flag or plan-config blob read on every request).

This pairs closely with how you model tenancy in the first place — if you have not settled that yet, multi-tenant SaaS architecture is the prerequisite read, because your tenant boundary defines your cache key prefix.

How do you namespace cache keys in a multi-tenant SaaS?

This is the single most important rule for multi-tenant caching: the tenant ID is part of the key, always.

bad:   user:42:profile          // which tenant's user 42?
good:  tenant:acme:user:42:profile

A flat user:42 key works in a single-tenant app and becomes a data-leak bug the moment two tenants can have overlapping internal IDs — or even when they cannot, because a cache poisoning or logic error now crosses a tenant boundary. Prefixing by tenant also gives you a clean blast radius: you can scan and drop tenant:acme:* when a tenant is deleted, downgraded, or needs a full cache flush, without touching anyone else.

A few namespacing conventions that pay off:

  • Put the tenant first: tenant:{id}:... so all of a tenant's keys sort together.
  • Add a schema version segment when the cached shape can change: tenant:{id}:v2:dashboard. Bumping v2 to v3 instantly invalidates every old entry after a deploy without a single delete.
  • Keep keys human-readable. You will be reading them at 2am during an incident.

The same per-tenant isolation discipline that governs your database rules should govern your cache. On Callidus the Firestore rules enforce the tenant boundary via JWT tenantId claims; a cache layer in front of any tenant-scoped store has to mirror that boundary or it silently defeats it.

What TTL should you set, and how do you handle invalidation?

TTL and invalidation are two halves of correctness. Think of them as belt and suspenders.

TTL (the suspenders): every key gets one. The TTL answers "if my invalidation logic has a bug, how long until it heals?" Pick it by how stale the data may safely be:

  • Per-request config / feature flags: 30–60s (or invalidate on change with a short TTL backstop).
  • User-facing dashboards and counts: 30s–5min.
  • Slow-changing reference data (plans, country lists): minutes to hours.
  • Never 0/no-expiry unless you have airtight invalidation and a memory budget.

Invalidation (the belt): on a write that changes cached data, delete the affected keys so the next read recomputes. The trap is partial invalidation — you update a row and delete tenant:x:user:5 but forget the tenant:x:dashboard aggregate that also counted that row. Two defenses:

  1. Keep an explicit map of "writes to entity E invalidate keys [...]" next to your write paths, so adding a new cached view forces you to register its invalidation.
  2. Lean on the version-segment trick for derived/aggregate keys you cannot enumerate cleanly — bump the version on deploy.

A related failure is the cache stampede: a hot key expires and a hundred concurrent requests all miss and hammer the database at once. The simple mitigation is a short per-key lock (one request recomputes, the rest briefly wait or serve the slightly stale value); for most SaaS at moderate scale, a small jitter on the TTL and an early-recompute window is enough.

Where does caching fit alongside the rest of the backend?

Caching is one stage in a request's life, not a standalone system. It sits behind your API layer and in front of your data store, and it interacts with everything around it: your rate limiting (a cache hit is cheap, so you may meter cached vs uncached differently), your background jobs (a job that recomputes an aggregate can warm the cache proactively), and your observability (cache hit rate is a first-class metric — a sudden drop usually means an invalidation bug or a deploy that changed key shapes).

Across projects the pattern holds. BookBed, my Flutter + Firebase booking SaaS that runs from one codebase across six OS platforms (iOS, Android, Web, macOS, Linux, Windows), leans on Firebase's own client-side caching for offline-tolerant reads; Pizzeria Bestek, a React + Supabase app, uses Supabase Realtime to push fresh data rather than caching aggressively — different freshness needs, different answers. There is no universal cache config; there is only "what is this read's cost, and how stale can it be?"

If you are building the surrounding pieces, the SaaS Backend Infrastructure pillar maps how caching, queues, webhooks, and rate limiting fit together, and multi-tenant SaaS architecture covers the tenant model your cache keys depend on.

FAQ

When should I add Redis caching to my SaaS? Add it when you have a measured problem: a query that is genuinely slow (aggregates, joins, full-text), data read far more often than it changes, or a downstream service with a cost or rate ceiling. Before that, a well-indexed database query is simpler and has one source of truth. Premature caching adds a second thing to keep consistent for no measured gain.

Cache-aside or write-through — which should I use? Cache-aside is the default for most SaaS: the app reads from cache, falls back to the database on a miss, and the cache only ever holds data someone actually requested. On writes, delete the affected keys rather than repopulating, and let the next read warm them. Reserve write-through for a small set of always-hot keys like feature flags or plan config that are read on nearly every request.

How do I keep cached data isolated between tenants? Put the tenant ID at the front of every cache key — tenant:{id}:... — so two tenants can never collide and so you can drop one tenant's entire cache with a prefix scan. A flat key like user:42 is how cached data leaks across tenant boundaries. The cache must mirror the same tenant isolation your database rules enforce; on Callidus that boundary is the JWT tenantId claim the Firestore rules check on every read.

What TTL should I set on cache entries? Set a TTL on every entry, chosen by how stale the data may safely be: seconds for dashboards and counts, minutes to hours for slow-changing reference data, 30–60s for per-request config. The TTL is your safety net for an invalidation you forgot — it guarantees wrong data self-heals in bounded time instead of persisting forever. Avoid no-expiry keys unless your invalidation is airtight.

Why does my cache still show old data after an update? Usually partial invalidation: you deleted the obvious key on write but missed a derived or aggregate key that also depended on the changed data — for example deleting the user key but not the dashboard count that included that user. Maintain an explicit map of which writes invalidate which keys, and for aggregates you cannot enumerate, use a version segment in the key (v2:) that you bump on deploy to invalidate everything at once.

DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps 2× faster than agencies using AI-augmented workflows. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

When should I add Redis caching to my SaaS?

Add it when you have a measured problem: a query that is genuinely slow (aggregates, joins, full-text), data read far more often than it changes, or a downstream service with a cost or rate ceiling. Before that, a well-indexed database query is simpler and keeps one source of truth. Premature caching adds a second thing to keep consistent for no measured gain.

Cache-aside or write-through — which should I use for a SaaS?

Cache-aside is the default for most SaaS: the app reads from cache, falls back to the database on a miss, and the cache only ever holds data someone actually requested. On writes, delete the affected keys rather than repopulating, and let the next read warm them. Reserve write-through for a small set of always-hot keys like feature flags or plan config read on nearly every request.

How do I keep cached data isolated between tenants?

Put the tenant ID at the front of every cache key — tenant:{id}:... — so two tenants can never collide and you can drop one tenant's entire cache with a prefix scan. A flat key like user:42 is how cached data leaks across tenant boundaries. The cache must mirror the same tenant isolation your database rules enforce; on Callidus that boundary is the JWT tenantId claim the Firestore rules check on every read.

What TTL should I set on cache entries?

Set a TTL on every entry, chosen by how stale the data may safely be: seconds for dashboards and counts, minutes to hours for slow-changing reference data, 30–60s for per-request config. The TTL is your safety net for an invalidation you forgot — it guarantees wrong data self-heals in bounded time instead of persisting forever. Avoid no-expiry keys unless your invalidation is airtight.

Why does my cache still show old data after an update?

Usually partial invalidation: you deleted the obvious key on write but missed a derived or aggregate key that also depended on the changed data — for example deleting the user key but not the dashboard count that included that user. Maintain an explicit map of which writes invalidate which keys, and for aggregates you cannot enumerate, use a version segment in the key (v2:) that you bump on deploy to invalidate everything at once.