Caching
The system has six distinct caching layers. The first four are stacked in series for leaderboard reads. The last two are orthogonal and apply to other subsystems.
Layer map
Layer 1 — Memory LRU
File: lib/leaderboard/request-cache.ts
| Property | Value |
|---|---|
| Storage | In-process LRU map |
| Capacity | 64 entries |
| TTL | 30 seconds |
| Scope | Single Vercel serverless instance (not shared) |
This is the fastest layer — a sub-millisecond in-memory lookup. Because Vercel functions are ephemeral and may have multiple instances, this cache is not shared across instances. Two concurrent users hitting different instances will each have their own LRU.
Stale-while-revalidate: if an entry is stale but present, the stale value is returned immediately and a background refresh is queued via after(). This avoids cache stampede and keeps latency low.
Layer 2 — Redis response cache
File: lib/leaderboard/request-cache.ts
| Property | Value |
|---|---|
| Storage | Upstash Redis |
| Key format | leaderboard:response:{installationId}:{scope}:{filters} |
| TTL | 86,400,000 ms (24 hours) |
| Value | Full serialized LeaderboardResponse object |
This is the cross-instance shared cache. When Layer 1 misses, this layer is checked. A hit here means no database or GitHub API access.
Graceful degradation: if Redis is unavailable (network error, missing env vars), the system falls back to Layer 1 (memory) or proceeds to Layer 3/4. Redis being down does not cause 500 errors.
Requires UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to be active. Without these, Layer 2 is skipped.
Layer 3 — Redis snapshot cache
File: lib/leaderboard/redis-cache.ts
| Property | Value |
|---|---|
| Storage | Upstash Redis |
| Key format | leaderboard:snapshot:preset:{id}:scope:{type}:entity:{type}:period:{p} |
| TTL | 86,400,000 ms (24 hours) |
| Value | { entries, computedAt, materializationId, rowCount } |
More granular than Layer 2. Where Layer 2 caches the assembled HTTP response, Layer 3 caches individual leaderboard snapshots keyed by scoring preset, scope, entity type, and period. This allows partial cache hits — if one preset’s data is fresh but another is stale, each can be served from cache independently.
Layer 4 — computed_scores database table
File: lib/supabase/leaderboard-db.ts
| Property | Value |
|---|---|
| Storage | Supabase Postgres computed_scores table |
| Freshness check | isPrecomputedScoresFresh(computedAt, TTL=24h) |
| Write path | writePresetComputedScores() with Redis advisory lock |
| TTL | 24 hours |
The computed_scores table stores precomputed leaderboard rows. This is the persistent cache — unlike Redis, it survives instance restarts and cold starts.
When writing new scores, an advisory Redis lock prevents multiple concurrent ingest jobs from writing duplicate rows. If the lock cannot be acquired, the ingest job exits early rather than duplicating work.
Freshness is checked against the computedAt timestamp in the row. If older than 24 hours, a fresh ingest is triggered.
Layer 5 — GraphQL response cache
File: lib/github/graphql-response-cache.ts
| Property | Value |
|---|---|
| Storage | In-memory LRU(64) + Upstash Redis |
| TTL | 90 seconds |
| Scope | Used by the /api/github/graphql proxy only |
Separate from the leaderboard cache stack. When a browser sends a GraphQL query through the proxy, the operation + variables are used as the cache key. Cached responses are served for 90 seconds before re-querying GitHub.
This reduces GitHub API rate limit pressure for common queries (e.g. fetching org members) that multiple users might issue at the same time.
Layer 6 — ETag cache
Table: repository_sync_state
| Property | Value |
|---|---|
| Storage | Supabase Postgres (one row per repository) |
| Columns | commits_etag, pulls_etag, issues_etag, comments_etag, reviews_etag |
| Mechanism | If-None-Match header on GitHub REST API calls |
During leaderboard ingest, each repository’s ETags are stored per endpoint. On subsequent ingest runs, these ETags are sent as If-None-Match headers to GitHub. If the data hasn’t changed, GitHub returns 304 Not Modified and no payload is transferred — the previous data is reused as-is.
This is not a time-based cache. It is conditional GET semantics. An endpoint’s data is considered fresh until GitHub says it changed.
Cache invalidation
| Layer | Invalidation trigger |
|---|---|
| Memory LRU (1) | TTL expiry (30s); process restart |
| Redis response (2) | TTL expiry (24h); explicit key delete |
| Redis snapshot (3) | TTL expiry (24h); explicit key delete |
| computed_scores (4) | TTL expiry (24h); new ingest overwrites rows |
| GraphQL cache (5) | TTL expiry (90s) |
| ETag cache (6) | GitHub returns new ETag on changed data |
There is no manual cache flush endpoint exposed by default. The ENABLE_DEBUG_ROUTES flag enables debug routes that expose cache state.