Skip to content

epic(perf/redis): make /api/bootstrap demand-driven — it ships a 3.2MB slow tier to every client, defeating the viewport + layer gates the frontend already has (successor to #5259) #5300

Description

@koala73

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:

  1. Panel data is viewport-gatedshouldLoad(id) = forceAll || this.isPanelNearViewport(id) (src/app/data-loader.ts:747). Below-the-fold panels don't fetch until you scroll near them.
  2. 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 ucdpEventsOFF 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 cyberThreatsOFF 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

  • Redis GET egress ≤ 17 GB/day sustained at peak-hour traffic, or an explicit decision to resize the plan
  • No client fetches a bootstrap key for a panel/layer it will not render
  • happy/tech/finance no longer receive geopolitical/energy/military keys
  • No duplicate-content keys read hot

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority, fix soonepicUmbrella tracking issue for multiple related child issuesperformancePerformance optimization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions