Skip to content

perf(bootstrap): R2 fast-tier read latency exceeds the mobile client budget from far regions — the incumbent-vs-R2 comparison is unmeasured (#5300) #5338

Description

@koala73

Follow-up to the R2 origin rollout (#5325, #5326, #5328, #5331) and the plan at
docs/plans/2026-07-14-001-perf-bootstrap-r2-origin-plan.md (KTD7, R8). The U3a production-shadow
measurement has now collected a full steady-state day, and it gives clear — but forked — guidance.
Serving is still Redis; this is about whether the fast tier can cut over.

TL;DR

  • R2 is reliable. Steady-state fallback rate 0.38% (2 unreadable + 6 timeout of 2,122 on 2026-07-15). The scary "16% unreadable" was a one-time 3-hour rollout window (2026-07-14 21:00 UTC, 407/407 = 100%, before creds/objects were wired), then 0% since. Not a steady-state problem.
  • The fast tier fails the latency gate from far regions. Its p99 exceeds the fast-tier mobile client abort (1,200 ms) in 9 of 19 Vercel regions.
  • But we're measuring the wrong thing. The gate compares R2 against an absolute 1,200 ms. The honest question is whether R2 is slower than the Redis read it replaces from the same region — and that number is not being logged, so the decision can't be made yet.

Evidence (Axiom wm_api_usage, event_type='bootstrap_r2_shadow', 2026-07-15 steady state)

R2 read latency, fast tier, by execution region vs the 1,200 ms mobile abort:

region p50 p99
hkg1 643 2,331
dub1 406 1,852
cdg1 369 1,839
bom1 768 1,732
icn1 761 1,672
sin1 682 1,536
cpt1 656 1,419
kix1 583 1,391
syd1 594 1,338
gru1 416 1,120
fra1 433 957
cle1 200 938
sfo1 328 724
iad1 188 408

near-enam (iad1/cle1/yul1): p99 938 ms. far (Asia/EU/Africa/AU): p99 1,664 ms. The bucket is single-region (enam); the Vercel edge is global. This is a cross-region S3 hop, and its cost is geographic.

Per the plan's KTD7 selection rule, there is no single fast-tier T that keeps P(L>T) ≤ 0.2% while T ≤ C_happy(fast) ≈ 1,150 ms, because the fast-tier p99 (1,631 ms global) already exceeds C_happy. The plan correctly blocks its own fast-tier rollout — this was flagged in KTD7 as "a real possible outcome, not a formality."

The slow tier is fine: p99 2,111 ms against a 3,000 ms client budget.

The gap that blocks the decision

The 0.38% fallback rate is measured against U3a's uncensored 5,000 ms probe ceiling. A real serving timeout would be bounded by C_happy (~1,150 ms fast), which would convert the far-region slow tail into fallbacks — several percent in ~9 regions, failing R8's 1% worst-cohort gate.

BUT today, every fast-tier CDN miss from Singapore already reaches cross-region to Redis (us-east-1). If that Redis read is already ~1.5 s from Singapore, then far-region mobile users already abort on cold fast-tier misses today, and R2 is not a regression — just cheaper. We cannot tell, because:

  • /api/bootstrap is not logged as a normal request event (no serving-duration telemetry).
  • The shadow event's duration_ms is null (the probe fires in waitUntil, decoupled from the parent request).

So the decisive number — incumbent Redis-on-miss latency by region — does not exist.

Remediation

Step 0 (unblocks everything): capture the incumbent. Add the parent request's Redis-assembly duration to the shadow event (the plan's U3a specified this via Server-Timing; it was not wired). One field. Collect a steady-state day. Then the fork resolves:

Branch A — R2 ≈ Redis from far regions (plausible; both are us-east-1 cross-region hops):
the C_happy gate is comparing R2 to an absolute the incumbent also breaches → no regression → ship both tiers as built. Full savings, no new architecture.

Branch B — R2 materially slower than Redis:

  1. Ship the slow tier now — it passes the gate on the current path (~1/3 of bootstrap egress, zero new work).
  2. Fast tier → Cloudflare Worker + R2 binding (Deferred item Batch market and RSS fetching with progressive updates #1 in the plan). api.worldmonitor.app is already behind Cloudflare and R2 is a Cloudflare product; a Worker with a native R2 binding reads the object from the same POP the request hit — no cross-region hop. The publisher, tier-key extraction, envelope, health, and fallback logic all stay as built; only the ~30 lines of "how the edge fetches the object" move from Vercel-aws4fetch to Worker-binding. The fast tier is ~2/3 of the savings, so this is worth building if Branch B holds.

Not a blocker, but note

Once the incumbent field is added, the 3-hour startup burst (100% unreadable) should be excluded from any calibration window — U3a's own "reject incomplete windows" discipline. The steady-state numbers above already exclude it (window starts 2026-07-15T00:00Z).

Acceptance

  • Shadow event carries the parent request's Redis-serving duration, per region.
  • One steady-state day of incumbent-vs-R2 latency recorded in the U3a evidence artifact.
  • Fork resolved with a one-line recommendation (Branch A ship-both, or Branch B slow-now + fast-via-Worker).
  • If Branch B: slow tier cut over and its fallback rate confirmed < 1% worst-cohort before the fast-tier Worker work begins.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions