Loading…
Loading…
Postmortems / PM-2026-10-11-11
Blameless postmortem. Incident record: incident-log.md INC-2026-10-11-11. Times UTC.
On 10-11 the prover began running out of memory on every attempt to prove the L2 block range right after the last finalized block (14832–14944). Each 113-block batch needs ~10 GiB in the prover; the kind node (27.4 GiB) had less than that free, so the kernel's global OOM killer killed the prover 48 times between 12:05 and 18:02. The prover's automatic recovery retries a crashed batch once and then parks it as .large for a human; all 22 queued batches ended up parked. With no .json request left, the prover went idle at 18:10. The coordinator, seeing that the highest consecutively proven block (14831) was more than 4,000 blocks behind the next block to conflate, stopped creating requests. The two components waited on each other and finalization stayed at L2 block 14831.
Alerts fired from 16:10 onward, but the only receiver is a log sink, so nobody saw them. The halt was found ~27 h after it began, while tracing W3 withdrawals during a learning session (376 withdrawals waiting to be claimed). Freeing memory (stopping the block explorer) and renaming the parked batches back to .json restarted proving; finalization resumed at 15:02 on 10-12.
This was the second occurrence in two days. The postmortem of the first (INC-08) had identified the same two fixes — size the prover's memory request to its real peak, and stop batches from overflowing normal mode — and neither had been done.
| what | how much |
|---|---|
| L2 blocks not finalized on L1 | 14832 → 31683 at detection: 16,852 blocks (~27 h of L2 history) |
| Withdrawals (L2 → L1) | 376 messages unclaimable (every withdrawal from L2 block 14848 onward); users would wait ~27 h + catch-up |
SLO settlement-freshness (99 %, 28 d) | error budget remaining −178 % at detection, −317 % during catch-up (budget exhausted many times over) |
| Conflation | paused from 10-11 ~18:10 (gap backpressure) |
| Not affected | L2 block production, user txs (W4), deposits (W2), RPC availability, SLOs other than settlement |
| Data | none lost: all L2 blocks, traces and requests were kept; proofs are being generated now |
Trigger: batch 14832–14944, the first batch after the last finalization, needed ~10 GiB of prover memory while the node had a few GiB free (MemAvailable fell to 0.3 GiB at 17:10 and ~0.06 GiB at 17:50).
Root causes (5 whys):
global_oom … Killed process (prover) anon-rss:9599592kB)..large, because blind retries under memory pressure destabilise the node. The prover controller does not process .large, and the coordinator stops conflating when the proven gap exceeds 4,000 blocks (BlockCreationMonitor … gapLimit=4000). Together they formed a deadlock that only a human can break.FinalizationStalled, ProverLargeRetry, settlement SLO pages, WorkflowStuck) all fired, but Alertmanager routes everything to an alert-sink that only writes a log. No person receives alerts.Contributing factors
ProverLargeRetry is a warning (ticket), although it always means "a human must act now".l2-node-besu, which can only rebuild state for the last bonsai-historical-block-limit = 1024 blocks (~1–2 h of traffic). Blocks older than that could not be traced, so a prover backlog beyond ~1024 blocks could not be worked off at all until the limit was raised during the recovery (INC-08's 1 h 22 min stall fitted inside it by luck).| time (UTC) | signal | seen by a person? |
|---|---|---|
| 10-11 12:05 | prover restart #1 (kube_pod_container_status_restarts_total) | no alert on restarts |
| 10-11 16:10 | LinethL2SLOBurnRatePage settlement-freshness (14.4x / 6x) | no (alert-sink) |
| 10-11 17:57 | LinethL2FinalizationStalled, LinethL2ProverLargeRetry | no |
| 10-12 01:00 | LinethL2WorkflowStuck (withdrawal finalization.wait) | no |
| 10-12 ~14:45 | found by hand: W3 withdrawal trace → postman 376 SENT → finalized block flat at 14831 | yes |
The monitoring itself worked: the SLO, the pipeline alerts and the workflow traces all pointed at the problem. The failure was routing: time to detect ≈ 26 h 50 min, of which ~22 h with critical alerts firing.
lineth_prover_requests (22 .large, oldest 26.8 h, nothing in progress), pod restartCount 48, lastState OOMKilled, kernel dmesg (global_oom), prover log (found no jobs in the queue since 10-11 18:10), coordinator log (gapOverflow=4001 gapLimit=4000)flux suspend kustomization lineth-l2-explorer; blockscout, blockscout-frontend → 014832-14944-getZkProof.json.large → .json.large → .jsonflux suspend kustomization lineth-l2-traffic; l2-load, l2-users → 0 (the 30 s heartbeat stays)BlockCreationMonitor … lastProvenBlock=15509 nextBlockToFetch=19510 gapLimit=4000flux resume … && reconcile --with-source); confirm postman claims the 376 withdrawalsWhat went well
stuck=true spans), kernel logs, and component logs each confirmed one link of the chain.What went wrong
Where we got lucky
LinethL2ProverLargeRetry → critical: a parked batch always needs a human
Alert when node MemAvailable < prover peak + margin while prover requests are pending (capacity alert before the OOM)
Postmortem review rule: a P0/P1 action from a postmortem is done or explicitly re-scheduled before the incident is closed
Capacity plan: prover throughput vs block production incl. liveness blocks; a second prover or larger node so catch-up after an outage is hours, not a day
partly done — 80-block normal-mode batches prove at ~1,440 blocks/h vs ~860–960 produced (INC-12); VM 30 GiB after host memory pressure; second prover open (INC-12 A6)
Alert on prover restarts (increase(kube_pod_container_status_restarts_total{container="prover"}[30m]) > 0)
Runbook: before re-enabling the explorer (or any big workload), compare free memory with the prover peak
Raise bonsai-historical-block-limit permanently (done at runtime via env, fcd76bc; size it to the longest prover outage we must survive) and alert when the proven gap approaches it
partly done — BESU_BONSAI_HISTORICAL_BLOCK_LIMIT=20000 is in Git (fcd76bc) and made the INC-12 re-conflation possible; the gap alert is open
Stop batches overflowing normal mode: lower conflation blocks-limit / trace limits, or run large mode by default
10-13 (INC-12: BLOCK_HASH = 1440, 80-block batches always in normal mode)
web3signer must fail (not run with 0 keys) when key files cannot be loaded, or retry: readiness probe on loaded key count / startup dependency on DNS+Vault; found when a node restart halted the chain 8 min
10-13 41a2d79 (wait-for-vault init container); held on the 10-13 04:17 restart
Route alerts to a person (Telegram receiver; severity=critical pages)
10-13 11:00 934a2bb: Telegram receiver on lab alerts (critical + warning, repeat 1 h); see INC-15
Prover memory request ~10 Gi (= measured peak) so the scheduler sees the overcommit; check the node fits it
10-13 1fb7983: request 10 Gi (normal-mode 80-block peak 9.44 GiB)
After catch-up: resume explorer + traffic, confirm 376 withdrawals claimed, close INC-11, restart CHG-2026-10-12-02 D3-b
— closed by the 10-12 chain reset (backlog and 376 pending withdrawals archived, not claimed); explorer and traffic resumed; CHG-02 D3-b not restarted
kube_pod_container_status_restarts_total).[10-11 17:50:15] oom-kill:constraint=CONSTRAINT_NONE … global_oom … Killed process 3777670 (prover) anon-rss:9599592kB.BlockCreationMonitor | Gap between highest consecutive proven block and L2 block is too big: lastProvenBlock=14831 nextBlockToFetch=18832 gapOverflow=4001 gapLimit=4000 (every second; nextBlockToFetch frozen at 18832 until the first proof on 10-12, then sliding: 15:17 lastProvenBlock=15509 nextBlockToFetch=19510).keep 14832-14944-getZkProof.json.large: already restored once; found no jobs in the queue from 10-11 18:10:27.DataFinalizedV3 before the halt at L1 block 0x1665d (10-11 11:52:36), start 14719, end 14831 (645th finalization).L2_TO_L1 SENT 376, min(sent_block_number) 14848.{ span.workflow = "withdrawal" && span.stuck = "true" }.