Successor to #5259 (closed — it had accumulated four rounds of measurement and three shipped fixes; this is the clean structural plan). Prereqs all shipped: #5250, #5262, #5263, #5287.
Where we are
Redis GET egress is down 87% (306 → 41 GB/day traffic-normalized) and commands down 79% (58.2 → 12.2 M/day). The #5249 regression is fixed. But we're still at ~53 GB/day ≈ 1.6 TB/month against a 500 GB/month plan (≈17 GB/day) — roughly 3× over.
The remaining cost is not a caching failure. The CDN hit ratio is a healthy 95–97%. It is that /api/bootstrap eagerly ships a 3.2 MB slow-tier payload to every client on every boot, and most of it is data that client will never render.
The core defect: bootstrap defeats two gates the client already has
The frontend is already demand-driven in two independent ways:
- Panel data is viewport-gated —
shouldLoad(id) = forceAll || this.isPanelNearViewport(id) (src/app/data-loader.ts:747). Below-the-fold panels don't fetch until you scroll near them.
- Map rendering is layer-gated — e.g.
if (this.ctx.mapLayers.ucdpEvents) { this.ctx.map?.setUcdpEvents(events) } (data-loader.ts:2640).
/api/bootstrap?tier=slow bypasses both. It fetches a fixed 83-key registry (api/bootstrap.js:465-466) with no variant filter and no demand filter, so every visitor pays for every key regardless of what their UI will actually show.
Evidence — the whales feed layers that are OFF by default in every variant
Default map-layer state, from src/config/panels.ts (only ~10 of 53 layers are ON by default in full):
| key |
size |
GB/day |
fires/cyberThreats/ucdpEvents default |
conflict:ucdp-events:v1 |
662 KB |
4.1 |
ucdpEvents — OFF in all 12 variant configs |
thermal:escalation:v1 |
467 KB |
2.9 |
below-fold panel, viewport-gated |
cyber:threats-bootstrap:v2 |
364 KB |
2.3 |
cyberThreats — OFF in all 12 variant configs |
energy:pipelines:{oil,gas}:v1 |
216+189 KB |
2.5 |
energy-domain, shipped to every variant |
energy:storage-facilities:v1 |
111 KB |
0.7 |
energy-domain, shipped to every variant |
Evidence — tiers are not variant-scoped
registry = Object.fromEntries(Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([k]) => tierSet.has(k))) — tier only, no variant. So the happy variant, whose own loader says "Happy variant only loads news data — skip all geopolitical/financial/military data" (data-loader.ts:754), still downloads UCDP events, cyber threats, and energy pipelines on every boot.
Evidence — cyber:threats:v2 and cyber:threats-bootstrap:v2 are byte-identical duplicates
Verified: same payload SHA, 950 threats each, 363.8 KB each. Both are read hot — ~4.3 GB/day to serve the same bytes twice.
The mechanism to fix this already exists and is already in production
/api/bootstrap?keys=<name> is live and used by 8 consumers today (AAIISentimentPanel, WsbTickerScannerPanel, progress-data, sanctions-pressure, renewable-energy-data, insights-loader, imf-country-data, country-intel). The whales simply were never migrated onto it.
Combined with the public=1 CDN pattern proven by #5250/#5287, a per-key public URL gives one CDN entry per key (~110 total — a bounded, well-behaved key space), each independently cached, each fetched only by clients that actually need it.
Proposed structure
Phase 1 — demote the off-by-default whales to on-demand (biggest win, lowest risk)
Remove from the slow tier; fetch via the existing ?keys=<name>&public=1 path when the panel enters the viewport or the layer is toggled on:
ucdpEvents, cyberThreats, thermalEscalation, pipelinesGas, pipelinesOil, storageFacilities.
~12.5 GB/day. No new machinery — reuses ?keys= + the public=1 CDN marker.
Phase 2 — collapse the duplicate cyber keys
One key, or make the bootstrap variant a compacted view of the canonical (the #5263 wildfire pattern). ~2–4 GB/day.
Phase 3 — variant-scope the tiers
?tier=slow&variant=happy should not contain energy/conflict/military keys. Cache key space stays tiny (6 variants × 2 tiers = 12 entries). Also fixes an obvious waste for happy/tech/finance.
Phase 4 — pre-compact the remaining whales at the seeder
Apply the #5263 wildfire pattern (compactWildfireDashboardPayload → dedicated *-bootstrap key) to conflict:ucdp-events (662 KB) and thermal:escalation (467 KB) for whatever still needs to ride in a tier.
Phase 5 — health:verdict:v1 compact snapshot (carried from #5259, new regression from #5262)
The memoized verdict stores the full 219-key checks map (~20 KB) and is GET-ed ~115 k/day = 2.34 GB/day, but ?compact=1 — every browser — needs only status + summary + problems (~1–2 KB). Persist a second compact snapshot key and serve ?compact=1 from it. ~2.2 GB/day for a few lines — best effort/reward ratio on this list.
Carried over from #5259 (still open, not yet done)
- Fast tier is 0.64 MB × 12,672 origin misses/day = 8.1 GB/day;
forecast:predictions:v2 (170 KB) is 2.2 GB/day of that.
- Consider longer
s-maxage on the slow tier (currently 7,200 s) once payloads shrink.
- Open decision: even with all phases we land ~15–25 GB/day. If that still misses 17 GB/day, the honest answer is to size the plan to the app rather than keep engineering. Decide explicitly rather than by attrition.
Measurement protocol (so results are comparable)
Acceptance
Successor to #5259 (closed — it had accumulated four rounds of measurement and three shipped fixes; this is the clean structural plan). Prereqs all shipped: #5250, #5262, #5263, #5287.
Where we are
Redis GET egress is down 87% (306 → 41 GB/day traffic-normalized) and commands down 79% (58.2 → 12.2 M/day). The #5249 regression is fixed. But we're still at ~53 GB/day ≈ 1.6 TB/month against a 500 GB/month plan (≈17 GB/day) — roughly 3× over.
The remaining cost is not a caching failure. The CDN hit ratio is a healthy 95–97%. It is that
/api/bootstrapeagerly ships a 3.2 MB slow-tier payload to every client on every boot, and most of it is data that client will never render.The core defect: bootstrap defeats two gates the client already has
The frontend is already demand-driven in two independent ways:
shouldLoad(id) = forceAll || this.isPanelNearViewport(id)(src/app/data-loader.ts:747). Below-the-fold panels don't fetch until you scroll near them.if (this.ctx.mapLayers.ucdpEvents) { this.ctx.map?.setUcdpEvents(events) }(data-loader.ts:2640)./api/bootstrap?tier=slowbypasses both. It fetches a fixed 83-key registry (api/bootstrap.js:465-466) with no variant filter and no demand filter, so every visitor pays for every key regardless of what their UI will actually show.Evidence — the whales feed layers that are OFF by default in every variant
Default map-layer state, from
src/config/panels.ts(only ~10 of 53 layers are ON by default infull):fires/cyberThreats/ucdpEventsdefaultconflict:ucdp-events:v1ucdpEvents— OFF in all 12 variant configsthermal:escalation:v1cyber:threats-bootstrap:v2cyberThreats— OFF in all 12 variant configsenergy:pipelines:{oil,gas}:v1energy:storage-facilities:v1Evidence — tiers are not variant-scoped
registry = Object.fromEntries(Object.entries(BOOTSTRAP_CACHE_KEYS).filter(([k]) => tierSet.has(k)))— tier only, no variant. So thehappyvariant, whose own loader says "Happy variant only loads news data — skip all geopolitical/financial/military data" (data-loader.ts:754), still downloads UCDP events, cyber threats, and energy pipelines on every boot.Evidence —
cyber:threats:v2andcyber:threats-bootstrap:v2are byte-identical duplicatesVerified: same payload SHA, 950 threats each, 363.8 KB each. Both are read hot — ~4.3 GB/day to serve the same bytes twice.
The mechanism to fix this already exists and is already in production
/api/bootstrap?keys=<name>is live and used by 8 consumers today (AAIISentimentPanel,WsbTickerScannerPanel,progress-data,sanctions-pressure,renewable-energy-data,insights-loader,imf-country-data,country-intel). The whales simply were never migrated onto it.Combined with the
public=1CDN pattern proven by #5250/#5287, a per-key public URL gives one CDN entry per key (~110 total — a bounded, well-behaved key space), each independently cached, each fetched only by clients that actually need it.Proposed structure
Phase 1 — demote the off-by-default whales to on-demand (biggest win, lowest risk)
Remove from the slow tier; fetch via the existing
?keys=<name>&public=1path when the panel enters the viewport or the layer is toggled on:ucdpEvents,cyberThreats,thermalEscalation,pipelinesGas,pipelinesOil,storageFacilities.~12.5 GB/day. No new machinery — reuses
?keys=+ thepublic=1CDN marker.Phase 2 — collapse the duplicate cyber keys
One key, or make the bootstrap variant a compacted view of the canonical (the #5263 wildfire pattern). ~2–4 GB/day.
Phase 3 — variant-scope the tiers
?tier=slow&variant=happyshould not contain energy/conflict/military keys. Cache key space stays tiny (6 variants × 2 tiers = 12 entries). Also fixes an obvious waste forhappy/tech/finance.Phase 4 — pre-compact the remaining whales at the seeder
Apply the #5263 wildfire pattern (
compactWildfireDashboardPayload→ dedicated*-bootstrapkey) toconflict:ucdp-events(662 KB) andthermal:escalation(467 KB) for whatever still needs to ride in a tier.Phase 5 —
health:verdict:v1compact snapshot (carried from #5259, new regression from #5262)The memoized verdict stores the full 219-key
checksmap (~20 KB) and isGET-ed ~115 k/day = 2.34 GB/day, but?compact=1— every browser — needs onlystatus+summary+problems(~1–2 KB). Persist a second compact snapshot key and serve?compact=1from it. ~2.2 GB/day for a few lines — best effort/reward ratio on this list.Carried over from #5259 (still open, not yet done)
forecast:predictions:v2(170 KB) is 2.2 GB/day of that.s-maxageon the slow tier (currently 7,200 s) once payloads shrink.Measurement protocol (so results are comparable)
EVALSHA/s (rate limiter). The old health-sweep proxy is dead by design since perf(health): memoize Redis verdict sweep #5262.energy:pipelines:gas:v1,energy:storage-facilities:v1).Acceptance
happy/tech/financeno longer receive geopolitical/energy/military keys