Case study

When to Cache and When to Query: Drawing the Line with Redis and PostgreSQL

Defining a caching strategy for a multi-tenant platform — what to cache, when to invalidate, and when to always hit the database.

RedisPostgreSQLNode.js

Context

Our platform serves dashboards and API responses that mix real-time data (user actions, live counts) with relatively static data (tenant config, reference data). Some endpoints were hitting PostgreSQL on every request and slowing down under load. We needed to reduce database load without serving stale data to users. The challenge was deciding what was safe to cache and what had to be fresh.

Constraints

  • Multi-tenant — cache keys must be tenant-scoped to avoid leaking data between clients
  • Some data changes frequently (user activity), some rarely (tenant settings)
  • No budget for a dedicated cache layer expert — we had to learn and ship ourselves

Architecture

We adopted a read-through cache pattern for specific entity types. User profiles, tenant config, and reference data (e.g. product categories) are cached in Redis with tenant-prefixed keys. TTLs vary: 5 minutes for user profiles (balance freshness vs load), 1 hour for tenant config, 24 hours for reference data. We invalidate on write — when a tenant updates their settings, we delete the cache key so the next read repopulates. For real-time data (live dashboards, activity feeds), we never cache; we query PostgreSQL and accept the cost. We also added a simple cache-aside helper so developers don't have to remember the invalidation rules.

Alternatives considered

  • Cache everything with short TTLs: Would have masked bugs — stale data would have been harder to debug. We wanted explicit control over what's cached and when it expires.
  • Write-through cache (update cache on every write): Adds complexity to every write path. Our write volume is low; invalidation on write is simpler and sufficient.

Lessons learned

  • Cache invalidation is hard — but if you limit what you cache, it's manageable. We only cache a handful of entity types.
  • Tenant-prefixed keys (tenant_123:user_456) prevent cross-tenant leaks and make debugging easier.
  • Document the caching rules. We added a small internal doc: 'If you add a new cached entity, add invalidation in the update handler.'