Appearance
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.
| Endpoint | maxAge | staleMaxAge | SWR | Handler |
|---|---|---|---|---|
/api/settings | 24h | 24h | no | server/api/settings.get.ts |
/api/competitions | 10s | 30s | no | server/api/competitions.get.ts |
/api/competitions/featured | 30s | 60s | no | server/api/competitions/featured.get.ts |
/api/competitions/[identifier] | 30s | 5m | no | server/api/competitions/[identifier].get.ts |
/api/competitions/[identifier]/entries | 30s | 5m | yes | server/api/competitions/[identifier]/entries.get.ts |
/api/winners/stats | 6h | 12h | yes | server/api/winners/stats.get.ts |
/api/winners/gallery | 30m | 1h | yes | server/api/winners/gallery.get.ts |
/api/winners/main-prize | 1h | 1h | yes | server/api/winners/main-prize.get.ts |
/api/winners/competition/[identifier] | 1h | 1h | yes | server/api/winners/competition/[identifier].get.ts |
/api/winners/ticker | 60s | 60s | yes | server/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.tslistens on the following channels and clears matching Nitro cache keys when a message arrives:cache:invalidate:competitioncache:invalidate:winnerscache: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:6379This 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?
- Verify
NUXT_REDIS_URLis set on the frontend service and points at the same Upstash instance the backend uses. - Check frontend server logs for
[Cache Subscriber]messages. In dev, init/runtime errors are logged at warn level. - Confirm the backend is actually publishing to the expected channels (
cache:invalidate:*) — check backend logs after the mutating action. - 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):
| Endpoint | Clamps |
|---|---|
/api/competitions/[identifier]/entries | page 1–1000, limit ≤ 100, search lowercase + trim + collapse whitespace + truncate 20 chars |
/api/winners/instant-wins/grouped | limit ≤ 100, offset ≤ 5000 |
/api/competitions/[identifier]/instant-wins/prize-groups/[groupKey]/winners | limit ≤ 100; cursor must match [A-Za-z0-9_=:.\-]{0,200} else cache is bypassed |
/api/competitions/[identifier]/instant-wins/prize-groups/[groupKey]/tickets | limit ≤ 100; cursor must match [A-Za-z0-9_=:.\-]{0,200} else cache is bypassed |
Helpers live alongside escapeKey in server/utils/cacheKeys.ts — clampLimit, clampOffset, clampPage, normaliseSearch.