Skip to content

Nitro Cache Invalidation

The frontend uses Nitro's built-in caching to serve frequently-accessed data faster and reduce backend load. When data changes in the backend, the cache is invalidated via Redis pub/sub — the frontend subscribes to invalidation channels on the shared Upstash Redis and clears the relevant Nitro cache entries when it receives a message.

Cached Endpoints

Values below are a snapshot of the current CACHE_MAX_AGE / CACHE_STALE_MAX_AGE constants in each handler; the authoritative source is the handler file itself under server/api/**/*.get.ts.

EndpointmaxAgestaleMaxAgeSWRHandler
/api/settings24h24hnoserver/api/settings.get.ts
/api/competitions10s30snoserver/api/competitions.get.ts
/api/competitions/featured30s60snoserver/api/competitions/featured.get.ts
/api/competitions/[identifier]30s5mnoserver/api/competitions/[identifier].get.ts
/api/competitions/[identifier]/entries30s5myesserver/api/competitions/[identifier]/entries.get.ts
/api/winners/stats6h12hyesserver/api/winners/stats.get.ts
/api/winners/gallery30m1hyesserver/api/winners/gallery.get.ts
/api/winners/main-prize1h1hyesserver/api/winners/main-prize.get.ts
/api/winners/competition/[identifier]1h1hyesserver/api/winners/competition/[identifier].get.ts
/api/winners/ticker60s60syesserver/api/winners/ticker.get.ts

Architecture

  • Publisher: Backend (kieron/competition-platform-backend) publishes messages to Redis channels after mutating data (settings updated, competition changed, winner published, etc.).
  • Subscriber: server/plugins/cache-invalidation-subscriber.ts listens on the following channels and clears matching Nitro cache keys when a message arrives:
    • cache:invalidate:competition
    • cache:invalidate:winners
    • cache:invalidate:settings
  • Transport: Upstash Redis (shared between frontend and backend). Frontend connects using NUXT_REDIS_URL (server-only runtime config).

There are no HTTP endpoints for cache invalidation and no shared secrets — the Redis connection itself is the trust boundary (authenticated via the Upstash credentials embedded in the connection URL).

Configuration

Only one env var is required for cache invalidation to work:

bash
NUXT_REDIS_URL=rediss://default:<password>@<host>.upstash.io:6379

This is the same Upstash instance the backend publishes to. If this var is missing, the subscriber plugin silently no-ops and the frontend falls back to TTL-only cache expiry.

Troubleshooting

Invalidations not happening?

  1. Verify NUXT_REDIS_URL is set on the frontend service and points at the same Upstash instance the backend uses.
  2. Check frontend server logs for [Cache Subscriber] messages. In dev, init/runtime errors are logged at warn level.
  3. Confirm the backend is actually publishing to the expected channels (cache:invalidate:*) — check backend logs after the mutating action.
  4. Use the Upstash CLI or console to manually PUBLISH cache:invalidate:settings '{}' and verify the frontend receives it.

Cache still serving stale data after invalidation?

  • The subscriber deletes explicit Nitro handler cache keys/patterns (see server/plugins/cache-invalidation-subscriber.ts), rather than looking up entries via a generic hash-of-params scheme. Make sure the invalidation message includes the expected identifier (slug/id) so it matches the key pattern the subscriber removes.
  • Nitro cache entries live in a bounded in-memory LRU per instance (see Storage bound below). Redis pub/sub ensures each frontend instance receives the invalidation message and clears its local cache.

Storage bound

The Nitro cache mount uses the unstorage/drivers/lru-cache driver, configured in nuxt.config.ts:

ts
nitro: {
  storage: {
    cache: { driver: 'lruCache', max: 10000 }
  }
}

max is an entry count, not a byte budget — at ~10 KB average entry size that caps the cache at ~100 MB worst-case, well under the 2 GiB Cloud Run heap floor. Tune the value in one place if production logs warrant it.

Steady-state key count is logged every 5 minutes by the cache-invalidation subscriber:

[Cache] keys=<n>

Charting this in Better Stack makes the ceiling (and any future leak) visible without a heap snapshot. A startup [Cache] driver=lruCache selftest=ok line confirms the driver is wired.

Cache-key cardinality

Four endpoints accept high-cardinality query params and clamp them at the cache-key layer (the upstream URL still receives the caller's original values verbatim, so backend semantics are unchanged):

EndpointClamps
/api/competitions/[identifier]/entriespage 1–1000, limit ≤ 100, search lowercase + trim + collapse whitespace + truncate 20 chars
/api/winners/instant-wins/groupedlimit ≤ 100, offset ≤ 5000
/api/competitions/[identifier]/instant-wins/prize-groups/[groupKey]/winnerslimit ≤ 100; cursor must match [A-Za-z0-9_=:.\-]{0,200} else cache is bypassed
/api/competitions/[identifier]/instant-wins/prize-groups/[groupKey]/ticketslimit ≤ 100; cursor must match [A-Za-z0-9_=:.\-]{0,200} else cache is bypassed

Helpers live alongside escapeKey in server/utils/cacheKeys.tsclampLimit, clampOffset, clampPage, normaliseSearch.