fix(health): bump correlationCards maxStaleMin 15→30 to absorb cron jitter#3640
Conversation
…itter Overnight UptimeRobot flipped DOWN/UP four times (00:56, 02:21, 04:02, 04:04 UTC), each time correlationCards STALE_SEED for 16-19 min. From the health:failure-log: 04:04 UTC WARNING correlationCards:STALE_SEED(19min) 02:21 UTC WARNING correlationCards:STALE_SEED(17min) 00:56 UTC WARNING correlationCards:STALE_SEED(16min) Math: cron is 5min (seed-bundle-derived-signals), bundle's 80% skip gate floors successive runs at 4min, maxStaleMin was 15 — exactly the gold-standard ⅓-of-stale-budget rule. No safety margin. When bundle runs lag 4-5 min (cold start, upstream latency, transient network), the gap between successful seeds stretches to 9-10 min; if the next run also lags slightly, total gap hits 15-19 min and trips STALE_SEED. Bumping to 30 gives 6× interval = 2× gold-standard floor. Same data cadence, no change to the cron, no change to the seeder. Just stops alerting on the natural jitter. No code-path or test changes — purely a tunable threshold.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryBumps
Confidence Score: 5/5Safe to merge — single-constant change with clear evidence backing it, no code-path impact. The change is one integer in a configuration object, well-supported by Redis failure-log data showing real overnight gaps of 16-19 min against a 15-min threshold. The new value (30 min) aligns with the 6× interval pattern used by comparable entries throughout the file. The only observation is that No files require special attention; Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as seed-bundle-derived-signals (5min cron)
participant Redis as Upstash Redis
participant Health as /api/health
participant UR as UptimeRobot
Note over Cron: Normal run T=0 → seed-meta written
Cron->>Redis: "write seed-meta:correlation:cards (fetchedAt=T+0)"
Note over Cron: Cold-start / upstream lag
Cron->>Redis: "write seed-meta:correlation:cards (fetchedAt=T+9)"
Note over Health: Probe at T+10
Health->>Redis: GET seed-meta:correlation:cards
Redis-->>Health: "fetchedAt=T+9 (1min ago — OK at maxStaleMin=30)"
Health-->>UR: "status=OK ✓"
Note over Cron: OLD behaviour (maxStaleMin=15)
Cron->>Redis: "write seed-meta:correlation:cards (fetchedAt=T+0)"
Note over Cron: Two lagged runs → gap stretches to 19min
Health->>Redis: GET seed-meta:correlation:cards
Redis-->>Health: "fetchedAt=T+0 (19min ago — STALE at maxStaleMin=15!)"
Health-->>UR: "status=WARNING ✗"
UR-->>UR: fire DOWN alert
|
Greptile review of PR #3640 flagged that `hyperliquidFlow` sits at the same 3× ratio (5min cron, 15min budget = floor) that just bit correlationCards overnight. Pre-fix comment incorrectly called it a "Railway cron"; it's actually bundle-driven (seed-bundle-market-backup), so it's subject to the same 80% skip-gate that produced the 9-19min jitter we just observed. Bumping proactively to the same 30min budget (6× interval) — no flap reports for hyperliquidFlow yet, but the underlying exposure is identical and the parity makes the rationale self-documenting. Also corrected the inline comment to reflect the bundle source.
|
Addressed Greptile's outside-diff P2 in
Now both 5-min-cron entries are at 30min budget (6× interval). Sweep confirmed no other tight ratios in the file: Updated PR scope: now bumps two entries ( |
…jitter) (#3652) * fix(health): bump resilienceIntervals/Ranking budget 720→840 (deploy-jitter) WM 2026-05-10 18:02 UTC: UptimeRobot flipped DOWN→UP within 1 minute, problems list showed `resilienceIntervals:STALE_SEED(722min)`. Diagnosed via /api/health failure-log + Railway cron history: 04:04 UTC ✓ (10s) 10:00 UTC ✓ (3s) 16:00 UTC ✗ NO ROW — Railway didn't fire the scheduled tick 22:03 UTC ✓ (9s) ← caught up here Multiple deploys today around 12:00 UTC (the missed slot was 12:00 UTC = 16:00 local in the user's TZ). Railway preempts scheduled cron ticks when a deploy is in flight — normal platform behavior, not a code bug. The 720min budget = 12h = 2× the real 6h cron cadence = exact gold- standard floor with ZERO jitter tolerance. Any single missed tick (12h gap) hits the threshold by definition. UptimeRobot polls every 60s and fires a DOWN page on any WARNING — even one that resolves within 1 minute. Bump 720 → 840 (14h ≈ 2.33× cadence): - tolerates 1 missed tick (12h gap) + ~2h deploy jitter - still alerts at 2 missed ticks (18h gap = real outage) - upper bound (1080) stays untouched per existing comment's note that 1080 = "over-permissive: masks a 12h outage" Same pattern as PR #3640 (correlationCards 15→30 and hyperliquidFlow 15→30): pre-fix all three sat at the exact 2-3× safety floor with no margin for cron jitter. Each was independently chosen for tight detection at design time; each independently produced UptimeRobot false-positives in practice. Tests: - Updated value-pin assertion (720 → 840) + comment with the 2026-05-10 incident as the new prior-value entry - Existing bounds checks (>= 540, <= 1080) unchanged and still passing - 13/13 resilience tests pass; 18/18 health-content-age tests pass * review: fix stale 720 refs + add resilienceRanking value-pin parity (P2×3) Greptile PR #3652 review surfaced three valid P2 issues in the test file, all stemming from the budget bump 720→840: P2 — Top-of-describe context comment (line 203-205) still read "720 = 12h staleness = 2 missed cron ticks". Updated to reflect the new 840 ≈ 14h with explicit jitter rationale. P2 — Bounds-check failure message (line 286-287) still said the project convention was "720 here, matching resilienceRanking". Updated to "840 (≈2.33× cadence, matches resilienceRanking)" so a future test failure reports the correct reference value. P2 — Missing value-pin assertion for resilienceRanking. api/health.js explicitly notes both keys MUST stay in sync ("written by the SAME Resilience-Scores section"). Without an assertion enforcing parity, a future one-off edit to resilienceRanking would silently diverge from resilienceIntervals and bypass the contract described in the comments. Added a fifth test in the describe block: it('resilienceRanking.maxStaleMin matches resilienceIntervals ...', () => { assert.equal(rankingBudget, intervalsBudget, ...); }); This catches divergence regardless of whether someone bumps just one of the two — the assertion enforces equality, not a hardcoded value, so the next legitimate bump only needs to touch both api/health.js values (the test passes as long as they match). 14/14 tests pass (was 13, +1 for the parity assertion).
* fix(health): bump resilienceIntervals/Ranking budget 720→840 (deploy-jitter)
WM 2026-05-10 18:02 UTC: UptimeRobot flipped DOWN→UP within 1 minute,
problems list showed `resilienceIntervals:STALE_SEED(722min)`. Diagnosed
via /api/health failure-log + Railway cron history:
04:04 UTC ✓ (10s)
10:00 UTC ✓ (3s)
16:00 UTC ✗ NO ROW — Railway didn't fire the scheduled tick
22:03 UTC ✓ (9s) ← caught up here
Multiple deploys today around 12:00 UTC (the missed slot was 12:00 UTC =
16:00 local in the user's TZ). Railway preempts scheduled cron ticks when
a deploy is in flight — normal platform behavior, not a code bug.
The 720min budget = 12h = 2× the real 6h cron cadence = exact gold-
standard floor with ZERO jitter tolerance. Any single missed tick (12h
gap) hits the threshold by definition. UptimeRobot polls every 60s and
fires a DOWN page on any WARNING — even one that resolves within 1
minute.
Bump 720 → 840 (14h ≈ 2.33× cadence):
- tolerates 1 missed tick (12h gap) + ~2h deploy jitter
- still alerts at 2 missed ticks (18h gap = real outage)
- upper bound (1080) stays untouched per existing comment's note that
1080 = "over-permissive: masks a 12h outage"
Same pattern as PR #3640 (correlationCards 15→30 and hyperliquidFlow
15→30): pre-fix all three sat at the exact 2-3× safety floor with no
margin for cron jitter. Each was independently chosen for tight
detection at design time; each independently produced UptimeRobot
false-positives in practice.
Tests:
- Updated value-pin assertion (720 → 840) + comment with the 2026-05-10
incident as the new prior-value entry
- Existing bounds checks (>= 540, <= 1080) unchanged and still passing
- 13/13 resilience tests pass; 18/18 health-content-age tests pass
* review: fix stale 720 refs + add resilienceRanking value-pin parity (P2×3)
Greptile PR #3652 review surfaced three valid P2 issues in the test
file, all stemming from the budget bump 720→840:
P2 — Top-of-describe context comment (line 203-205) still read
"720 = 12h staleness = 2 missed cron ticks". Updated to reflect the
new 840 ≈ 14h with explicit jitter rationale.
P2 — Bounds-check failure message (line 286-287) still said the
project convention was "720 here, matching resilienceRanking". Updated
to "840 (≈2.33× cadence, matches resilienceRanking)" so a future test
failure reports the correct reference value.
P2 — Missing value-pin assertion for resilienceRanking. api/health.js
explicitly notes both keys MUST stay in sync ("written by the SAME
Resilience-Scores section"). Without an assertion enforcing parity, a
future one-off edit to resilienceRanking would silently diverge from
resilienceIntervals and bypass the contract described in the comments.
Added a fifth test in the describe block:
it('resilienceRanking.maxStaleMin matches resilienceIntervals ...', () => {
assert.equal(rankingBudget, intervalsBudget, ...);
});
This catches divergence regardless of whether someone bumps just one
of the two — the assertion enforces equality, not a hardcoded value,
so the next legitimate bump only needs to touch both api/health.js
values (the test passes as long as they match).
14/14 tests pass (was 13, +1 for the parity assertion).
* fix(health): bump gdeltIntel maxStaleMin 420→720 (deploy-jitter, same as #3652)
2026-05-12 15:20 UTC: UptimeRobot flipped DOWN→UP within ~1 minute,
failure-log showed `gdeltIntel:STALE_SEED(421min)` → cleared, then
hit again at 467min. Same Railway-deploy-preempted-tick pattern as
resilienceIntervals on 2026-05-10 (PR #3652).
Root cause: maxStaleMin=420 at 6h cron cadence = 1.16× ratio with
virtually zero jitter margin. Any single missed cron tick (deploy
preempts a scheduled fire) produces a >7h gap → flips STALE_SEED for
the ~1 minute until the next scheduled run catches up.
Bumping to 720 (12h = 2× cadence) matches the gold-standard project
convention:
- tolerates 1 missed tick (12h gap) at 6h cadence
- alerts at 2 missed ticks (18h gap) — real outage signal preserved
- upstream CACHE_TTL is 24h, so per-topic merge always has a prior
snapshot even at the upper end of the new budget
No tests pin this value, so the only change is the production
constant + the audit-trail comment.
Fifth instance of the same class today/this week (correlationCards +
hyperliquidFlow in #3640, resilienceIntervals/Ranking in #3652, now
gdeltIntel). Pattern: knobs sized at the exact safety floor have no
room for routine Railway deploy jitter.
Summary
One-line tuning fix. Stop overnight UptimeRobot flips on
correlationCards.Evidence
Read
health:last-failure+health:failure-logfrom Upstash this morning. Four WARNING incidents overnight, all the same root cause:UptimeRobot pages exactly correlate (DOWN durations of 2-13min match the time between flip and the next successful seed catching up).
Math
seed-bundle-derived-signalsmaxStaleMinmaxStaleMinWhen a bundle run lags by 4-5 min (cold start, upstream latency, transient network), the next successful seed lands at minute ~9-10 instead of ~5. If the next run lags too, the gap stretches to 15-19 min — over the budget by 1-4 min, just long enough for one /api/health probe to flip WARNING and UptimeRobot to fire DOWN.
The data itself doesn't go stale at 15 min — the cadence is fine. Only the alert threshold was too tight.
Changes
One field, one file:
No code-path change, no test change — purely a tunable threshold.
Test plan
health:failure-logRedis data (the 16-19min gaps would not have tripped at maxStaleMin=30)health:failure-lognext morning — entries for correlationCards:STALE_SEED should be gone unless the gap exceeds 30min (which would be a real outage worth paging on).Companion (separate PR)
Surfacing
health:failure-logvia a?history=1query param so this kind of diagnosis doesn't require Upstash credentials. Tracking in a separate PR to keep this one minimal.