docs/solutions/best-practices/egress-cost-tracks-origin-miss-rate-not-client-count.md
Epic #5300 (opened and closed COMPLETED 2026-07-14) started from a single alarming number: Redis GET egress was running roughly 3x over the plan allowance. The obvious first hypothesis — "the cache is failing, misses are leaking to origin" — was ruled out immediately by the field data: the CDN was reporting a healthy 95–97% hit ratio. Caching was working. The bytes were still leaving.
The reflex when egress is high is to reach for the biggest payloads and shrink them — "reduce payload" phases. The bootstrap slow tier was the eager suspect: per the #5300 investigation it was shipping on the order of 3.2 MB to every visitor. (The tree today reflects the landed arc — src/services/bootstrap.ts:361 documents the slow tier at "~410 KB" and :389 at "~500 KB", after the dossier split described below.) Payload-shrink phases were scoped, sized in GB/day, and slated for engineering weeks.
The lesson of the epic is that most of that scoping was arithmetic that had not been done yet. Two of the "reduce payload" phases were mis-sized by the same mistake — reasoning about who reads a key or how many clients load it instead of how many origin misses the key incurs and how big each transferred body is. One phase was voided outright before any code was written; another was under-scoped by nearly 4x because a periodic refresh path had been silently bypassing the CDN. Both fall out of one test.
The lever test. Before scoping any egress, bandwidth, or cost-reduction work on a cached key-value payload, write the cost as:
egress ≈ origin-miss count × transferred payload size
Client count, reader count, and total request volume are not on the right-hand side — the CDN absorbs them. The only two levers that move egress are the origin-miss rate (requests that fall through the cache to Redis/origin) and the bytes per miss. If a proposed change does not reduce one of those two numbers, it does not reduce egress, no matter how much storage or how many requests it touches. Run this test on every proposed phase and discard the ones whose math nets to zero.
Three facets make the test operational.
Facet 1 — Deduplicating byte-identical keys saves storage, not bandwidth. Two keys can be byte-for-byte identical (same SHA, same size) and still cost exactly what they cost after you collapse them, because the readers are what drive egress, not the stored copies. In the epic, cyber:threats-bootstrap:v2 and cyber:threats:v2 were byte-identical — same SHA, 950 threats, 363.8 KB each — but they had different readers: bootstrap read the -bootstrap key and the RPC read the canonical one. This split is still visible in the tree: api/health.js:85 maps the bootstrap reader to cyber:threats-bootstrap:v2 while api/health.js:215 maps cyberThreatsRpc to cyber:threats:v2. Collapsing the two into one key would leave the same two readers issuing the same number of GETs for the same number of bytes — total egress unchanged. Storage would halve (one 363.8 KB copy instead of two); bandwidth would not move. The epic's planned "Phase 2 (~2–4 GB/day)" rested entirely on this dedup and was voided by the arithmetic before a line of code was written.
Facet 2 — When a hydration payload is one-shot, every periodic refresh path must be audited for un-CDN'd fallthrough. forecast:predictions:v2 was scoped at 1.3 GB/day — its share of the bootstrap slow tier — but actually cost ~4.9 GB/day. The gap was a refresh path nobody had put on the ledger. The client hydrates once from getHydratedData(), which is one-shot by design: it reads the value and immediately deletes it (src/services/bootstrap.ts:67-71 — getHydratedData calls hydrationCache.delete(key) on read). So the client's 30-minute refresh tick found nothing in the hydration cache and fell through to the get-forecasts RPC. Per the #5300 investigation that RPC had no CDN shield: every refresh was an origin miss — roughly 17.5k uncached origin reads/day at 188 KB each (the 188 KB figure is grounded in scripts/_forecast-dashboard.mjs:4). The one-shot hydration guaranteed that the recurring path could never be a cache hit; it was origin traffic by construction. PR #5311 fixed both halves — the bootstrap share and the RPC fallthrough. The fix's shape is now codified in the tree: ensureHydrated (src/services/bootstrap.ts:91-116) fetches on-demand keys through a CDN-shielded public URL (/api/bootstrap?keys=<name>&public=1) and its docstring states the rule directly — "This must NOT fall back to the domain RPC: the RPC reads the same Redis key with no CDN in front of it, so routing misses there would relocate the egress rather than remove it."
Facet 3 — A measurement protocol that makes results comparable. The epic's numbers held up because the measurement was disciplined:
EVALSHA rate tracks request volume independent of payload, so it normalizes measurements taken under different load.energy:pipelines:gas:v1 (api/health.js:287) is one — so its origin GETs measure the miss rate itself, uncontaminated by RPC or MCP readers.Three hard-won refinements to that protocol from the epic's own sessions (session history; the first and third are recorded only in the working sessions, not in the issue trail):
seismology:earthquakes:v1) turned out to also be a heavily-read public RPC route; it was replaced with correlation:cards-bootstrap:v1 once the contamination was found. A probe key is only a clean origin-miss meter if bootstrap is its only reader.misses ≈ shards × 86400/TTL was explicitly tested and shown wrong: the fast tier's 12× shorter TTL produced only ~1.6× more origin misses, not 12×. POP request distribution, not TTL arithmetic, dominates.The cost of the lever test is about thirty minutes of arithmetic. The cost of skipping it, in this epic, was measured in engineering weeks pointed at the wrong targets.
forecast:predictions:v2 was on the books at 1.3 GB/day and was actually ~4.9 GB/day. Had the team trusted the bootstrap-share estimate, they'd have "fixed" the payload, watched egress barely move, and been mystified — because the dominant cost was a refresh path that was never in the model (Facet 2). Thirty minutes of tracing the one-shot hydration to its fallthrough recovered the missing 3.6 GB/day before the work was scoped, not after it disappointed.The through-line: egress is an economics problem, and the units are miss-rate and payload-size. Any optimization expressed in other units — number of keys deduplicated, number of clients served, requests handled — is measuring the wrong thing, and the plan built on it will over- or under-deliver by whatever the CDN happens to be absorbing.
Non-fix: the cyber dedup that saves storage, not bandwidth.
cyber:threats-bootstrap:v2 and cyber:threats:v2, both 363.8 KB, same SHA, 950 threats. Bootstrap reads the first (api/health.js:85); the RPC reads the second (api/health.js:215). Two readers, two GET streams.Non-fix: turning panel defaults off (session history — this exchange is recorded in the epic's working sessions, not on GitHub).
/api/bootstrap filters purely by tier and has no visibility into panel settings — the key ships in the payload whether the panel is on, off, or nonexistent, and the slow tier incurs its ~6,048 origin misses/day regardless of whether 100 or 100,000 people load the app. Flipping a default changes what the browser renders, not a single byte Redis serves.Real fix: the forecasts fallthrough (PR #5311).
forecast:predictions:v2 is 188 KB (scripts/_forecast-dashboard.mjs:4). Scoped at 1.3 GB/day (bootstrap share). The client hydrates once via the one-shot getHydratedData() (src/services/bootstrap.ts:67-71); its 30-minute refresh tick finds an empty hydration cache and falls through to the un-CDN'd get-forecasts RPC — ~17.5k origin reads/day × 188 KB, so the true cost was ~4.9 GB/day (per the #5300 investigation).caseFile evidence off the hydration list — the bootstrap key carries the list the panel renders, dropping 188 KB → 41 KB, while the canonical key keeps the dossiers for the RPC/MCP/chat readers (scripts/_forecast-dashboard.mjs:9-21). The refresh path was put behind the CDN: on-demand keys now fetch through ensureHydrated → /api/bootstrap?keys=<name>&public=1 with a CDN cache entry per key (src/services/bootstrap.ts:91-116), and the bootstrap endpoint gives on-demand keys the slow-tier CDN profile rather than a tier-less default (api/bootstrap.js:459). The RPC is no longer the recurring reader; the periodic tick is a cache hit.../2026-07-14-bootstrap-r2-economic-comparison.md — project-specific cost record whose workload arithmetic (origin misses × measured response sizes → GB/day → dollars) operationalizes this doc's lever for the R2 origin decision (#5325).../2026-07-14-bootstrap-r2-timeout-measurement.md — extends the probe-key technique (bootstrap:r2-shadow-origin-marker:<tier>) to reconcile R2-shadow vs Redis-canonical traffic.src/services/bootstrap.ts:83-89.scripts/_forecast-dashboard.mjs — the dashboard-vs-canonical forecast split and the 188 KB → 41 KB reasoning.api/health.js — the reader map that makes the byte-identical-but-two-readers structure legible (:85, :107-108, :215, :287).