ClickHouse retention scans OOM-killed the analytics store — root cause and fix (2026-07-24)

2026-07-24  ·  clickhouseanalyticsincidentperformanceinfrastructure

What happened

All analytics dashboard loads got slow after the Fly migration ("Railway was faster") — retention on big games took 40–110s or timed out. Real cause was not Fly vs Railway: ClickHouse (quest-system-clickhouse, 8 GB Fly box) was hitting its server-wide ~7 GiB memory cap on retention reads. The OvercommitTracker killed the biggest queries — and sometimes innocent bystanders, including 2 MiB INSERTs, so ingested events silently landed Postgres-only and CH drifted. Every killed/timed-out read silently fell back to the near-empty Postgres mirror, which is why dashboards were slow and sometimes wrong rather than erroring.

Root cause (three factors, all required)

  1. retention_events_ch filtered only game_id + occurred_at >= now()-30d. The raw TTL is also 30 days, so the time filter matched ~every row of the game.
  2. quest.game_events sort key is (game_id, event_type, occurred_at, id)ORDER BY occurred_at can't stream in index order across event-type runs, so LIMIT 50000 applies only after a full sort. Planner-verified: 206,005,819 rows read for one retention call on the 189M-row world-cup-event-testing game; with an event-type filter: 5,025,217 (41×).
  3. ClickHouse's memory cap is server-wide (shared with merges + inserts + all concurrent reads) — one runaway read starves everything else.

Fix (PR #9, fix/ch-retention-memory)

Context / when to revisit