Skip to content

Blog

Published Jul 19, 2026 · Updated Aug 6, 2026

Ethereum client bake-off

“The Fastest Ethereum Client Is One Almost Nobody Runs”

A field campaign that began with a 23-day measurement phase (2026-06-22 → 2026-07-14) and continued with steady-state and restart-resume measurements through 2026-08-03, comparing seven execution-client syncs and five consensus clients — the same mainnet sync, on the same host, one client at a time, recording two numbers for each: final synced disk footprint and sync duration. The interesting part is what fell out of it: an operability axis that turns out to matter more than either headline number, and a genuine paradox — the client that synced fastest in the whole field has essentially zero real-world adoption.

Companion post: this write-up covers the clients and the numbers. For the agent orchestration, the harness, and the methodology behind them, see How We Ran a Six-Week Ethereum Client Bake-Off With Claude.

TL;DR

Disk: the field converges, ~1.0–1.2 TiB

Every EL that carries full post-merge history lands in the same band (geth 1.13 TiB, nethermind ~1.06 TiB, besu 1.08 TiB) — disk size is set by history-retention config, not client efficiency, so it isn't a good axis for picking a winner.

Speed winner — ethrex, ~2h16m

The fastest cold sync in the field by a wide margin; the next fastest was geth at ~8h28m.

The twist — ethrex's restart-resync cliff

Gaps through 23 minutes / 124 blocks resumed, while a 26-minute / 132-block gap stalled. Measured 1.5–2-hour gaps discarded synced state and triggered a full re-snap (~2h). This operability tax is the likely reason the fastest-syncing client in the field has close to zero real-world mainnet adoption.

The CL layer looks solved — on the axes we measured

All five consensus clients checkpoint-synced to a validating head in minutes — about 6–9 minutes on the geth anchor, ~7–10 minutes on the nethermind anchor, and ~22–23 minutes on the ethrex anchor. All five reached a validating head on all three anchors (three runs needed a caveat along the way — teku's JVM heap sizing and a watchdog false positive, grandine's harness du bug, and lodestar's anchor-gap re-read — none of them client faults). Footprint is the main differentiator.

Cold-sync time, at a glance

Completed execution-client syncs only — the two 72-hour-capped clients (reth, nimbus_eth1) and erigon's deadlocked run have no comparable sync time.

Ethrex
2h 16m
Geth
8h 28m
Nethermind
~14h 30m
Besu
19h 18m

Bars use a shared 0–20 hour scale.

Disk footprint, at a glance

All seven execution clients. Hatched bars aren't a comparable finished footprint — partial (72h-capped), frozen (erigon's no-sync deadlock), or no-history (ethrex, which plateaus at ~470–476 GiB but serves no history) — so a short hatched bar isn't a win: Nimbus-eth1's ~40 GB is only ~21% of a sync, reth's ~0.98 TiB is a 72h-capped partial (projected to land in the same band as the solid bars once finished), and ethrex's ~470–476 GiB is a settled plateau, not a pruned-comparable footprint — it's smaller only because it retains no history at all, not because it's more efficient. The three solid bars (nethermind, besu, geth) converge in the same ~1.0–1.2 TiB band once full post-merge history is retained — disk size is set by that retention config, not client efficiency.

Nimbus-eth1
~40 GB (partial)
Ethrex
~475 GiB (no-history)
Reth
~0.98 TiB (partial)
Nethermind
~1.06 TiB
Besu
~1.08 TiB
Geth
~1.13 TiB
Erigon
~1.21 TiB (frozen)

Bars use a shared 0–1300 GiB scale.

EL scorecard

Each execution-client run used a fixed Prysm consensus client and a 72-hour cap.

Nethermindsynced
Sync time
~14.5h
Footprint
~1.06 TiB steady-state (~251 GiB pre-backfill)
Sync mode
snap + Halite
Mainnet share
36.0%
Gethsynced
Sync time
~8h28m
Footprint
~1.13 TiB (pruned)
Sync mode
snap + --history.chain postmerge
Mainnet share
44.9%
Ethrexsynced
Sync time
~2h16m — fastest
Footprint
~470 GiB steady-state plateau (no-history)
Sync mode
snap (v19.0.0 at sync; v22.0.0 steady-state)
Mainnet share
~0%
Besusynced
Sync time
~19h18m
Footprint
~1.08 TiB
Sync mode
snap / Bonsai
Mainnet share
17.4%
Rethhit 72h cap (47% by block, ~21% gas-weighted)
Sync time
did not finish
Footprint
~0.98 TiB (partial)
Sync mode
full-sync-only
Mainnet share
1.5%
Nimbus-eth1hit 72h cap (~21.6%)
Sync time
did not finish
Footprint
~40 GB (partial)
Sync mode
full-sync-only
Mainnet share
~0%
Erigonno-sync
Sync time
deadlocked
Footprint
~1.21 TiB (frozen partial, not a synced datadir)
Sync mode
OtterSync
Mainnet share
~0%

Mainnet execution-client share, sampled once during the campaign; not re-checked.

CL scorecard

Each consensus client ran against a fixed EL anchor, and the full sweep was repeated against two more anchors (ethrex, then geth, then nethermind) to test EL/CL decoupling directly; all five synced on all three.

Lighthouse
Ethrex anchor
~739 MiB — smallest
Geth anchor
~518 MiB
Nethermind anchor
~470 MiB
History-prune levercheckpoint-sync-url
Lodestar
Ethrex anchor
~827 MiB
Geth anchor
~177 MiB — smallest
Nethermind anchor
~178 MiB — smallest
History-prune leverpruneHistory=true
Grandine
Ethrex anchor
~946 MiB actual (sparse file)
Geth anchor
~725 MiB actual (sparse file)
Nethermind anchor
~730 MiB actual (sparse file)
History-prune lever--prune-storage
Teku
Ethrex anchor
~2.1 GiB
Geth anchor
~936 MiB
Nethermind anchor
~848 MiB
History-prune leverdata-storage-mode=minimal
Nimbus
Ethrex anchor
~5.0 GiB — largest
Geth anchor
~1.2 GiB — largest
Nethermind anchor
~1.3 GiB — largest
History-prune leverhistory=prune

The same client's three columns differ because absolute footprint tracks how long the CL had been following the chain when it was sampled, rather than which EL it paired with. The geth- and nethermind-anchor runs were measured minutes after checkpoint-sync, on a fresher datadir. It is the broad tiers (lightweight, mid, heavy), not the absolute size or exact within-tier order, that reproduce across anchors.

The CL tiers also reproduced across three different EL anchors: a lightweight pair (lodestar, lighthouse), a mid pair (grandine, teku), and nimbus alone at the heavy end. Within-tier order is measurement-window-sensitive — teku itself moved ~667 → ~848 MiB across two runs on the same nethermind anchor, crossing grandine (~730 MiB) and back.

Additional run details

The scorecards above are the curated view. This table adds peer counts, config-optimality verification, re-run history, and other notable per-candidate detail.

geth × prysm
Peers
Config optimal
yes
Re-runs
0

Baseline; no large optimistic gap to close

nethermind × prysm
Peers
49
Config optimal
yes
Re-runs
1

First attempt: 13.3h 0-peer loopback stall; re-run after ExternalIp fix synced clean. Restart-resume measured and bisected (2026-08-01→03): every gap from 12 min to ~35h resumed by plain block import, no re-snap, no cliff

ethrex × prysm
Peers
50
Config optimal
yes
Re-runs
0

Datadir plateaus at ~470–476 GiB (confirmed by a follow-up steady-state measurement run on v22.0.0, 4h09m56s snap, drifting 470.2→475.5 GiB over ~42h after); 1 auto-healed stale-pivot event; serves no history beyond its snap pivot

besu × prysm
Peers
~50
Config optimal
n/a from the harness gate; sync-flag audit: SNAP + Bonsai ✅
Re-runs
2

Un-pruned run synced clean and is the ranked footprint; the pruned re-run deadlocked twice and was abandoned (no verdict)

reth × prysm
Peers
Config optimal
yes
Re-runs
1

578 samples; relaunched after --full fix; 47% by block / ~21% gas-weighted at cap

nimbus-eth1 × prysm
Peers
20–25
Config optimal
yes
Re-runs
1

72h continuous, 0 restarts; supersedes an earlier ~21 GB aborted run

erigon × prysm
Peers
Config optimal
n/a (no-sync)
Re-runs
0

CPU cap raised 200%→600% mid-run; advanced ~5k blocks then re-froze

CL sweep × ethrex anchor (5 CLs)
Peers
Config optimal
yes (all 5)
Re-runs
2

teku: JVM-OOM on first attempt (TEKU_CACHE fix); grandine's first attempt failed on a harness du bug

CL sweep × geth anchor (5 CLs)
Peers
Config optimal
yes (all 5)
Re-runs
0

Cross-anchor confirmation re-run; the lightweight/mid/heavy tiers reproduced (lodestar↔lighthouse swapped within the lightweight pair, vs. the ethrex primary)

CL sweep × nethermind anchor (5 CLs)
Peers
Config optimal
yes (all 5)
Re-runs
1

Second cross-anchor confirmation; teku re-measured (~667→~848 MiB across two runs on the same anchor, showing how window-sensitive the mid tier is); lodestar was re-measured after the anchor returned to head (~7m36s / ~178 MiB); its first attempt (~76m14s) was an anchor-gap artifact, not a lodestar property

Sourced from CLIENT_BAKEOFF_RESULTS.md, the campaign's source-of-truth data — see the full results on-site or the raw doc on GitHub.

What we measured, and how we kept it honest

The campaign began with a 23-day measurement phase (2026-06-22 → 2026-07-14) and continued with steady-state and restart-resume measurements through 2026-08-03, all on a shared semi-production host (not a live validator), with MEV disabled and no validator keys. The bake-off measures, for each client, the final synced disk footprint and the sync duration: one candidate at a time, a 72-hour cap per candidate, and the footprint taken from the last near-cap du sample — never the peak mid-sync.

For the EL scorecard we hold the CL constant at Prysm. That's defensible because an EL's footprint and sync time are EL-only properties, decoupled from the CL across the Engine API — the Prysm datadir ran ~0.65–1.68 GB in the bounded runs (up to ~12.5 GB during reth's full 72h cap), still negligible against an EL's hundreds of gigabytes. We confirmed the decoupling empirically later (see the CL matrix above), so this isn't just an assumption.

The honesty mechanism

Early in the campaign we corrupted our own results by recording footprints before verifying each client was running in its most disk-efficient mode. A benchmark that measures your misconfiguration instead of the client is worse than no benchmark. So we built a config-optimality gate into the harness: it inspects the actually-generated, actually-running config and refuses to trust a footprint from a mis-configured client, stamping every row config_optimal=yes|no. The gate itself needed six bug-fixes across three review rounds before we trusted it — a measurement tool gets no more benefit of the doubt than the thing it measures. Every comparable footprint on this page comes from a synced run whose disk configuration was verified — by the harness config_optimal gate, or (for besu, whose harness token targets a prune lever the ranked run deliberately didn't use) by the manual sync-flag audit (capped, no-sync, or pruned-only runs are marked as such and excluded from the ranking).

A knock-on benefit of that gate: it forced us to empirically settle config questions we'd otherwise have guessed at. The clearest example is nimbus_eth1's history-pruning flag (below), where the binary's --help and the online docs flatly contradicted each other — and only a live run resolved it. See how we tested this with Claude for the full harness engineering process.

The disk story: there is no winner — the field converges

Nethermind's synced-tip snapshot read ~251 GiB, well below geth's ~1.13 TiB — but that number was taken before nethermind's FastBlocks finished backfilling post-merge block bodies and receipts. Its steady-state datadir (re-measured 2026-08-01) is ~1.06 TiB (~1,088 GiB): state ~226–230 GiB (its compact Halite/Paprika flat storage) plus ~843 GiB of post-merge bodies and receipts plus ~19 GiB of headers and code, the same history geth keeps under --history.chain postmerge. Under matched history-retention configs, nethermind and geth are on par.

That history is a config choice, though: as of 2026-08-03 the shipped default turns it off (NETHERMIND_FULL_HISTORY=false) — a fresh minimal-history sync drops the post-merge bodies and receipts and holds at ~250–280 GiB (state only) with no backfill, in exchange for serving no history (pre-sync blocks return null, like ethrex). It is the same retention lever turned down — not a client that is “smaller” — and you turn it back on with NETHERMIND_FULL_HISTORY=true on a fresh/rebuilt datadir for a public RPC; changing an existing minimal datadir requires a rebuild. The ~1.06 TiB figure and the chart below are that full-history opt-in.

Among the measured no-history configurations in this campaign, nethermind is the smallest staking node — ~250–280 GiB against ethrex's ~470 GiB, and a floor geth cannot reach (it has no clean lever to drop post-merge history) — the one place its compact state engine is a genuine disk win, paid for by serving no history. It is not a claim that nethermind is 4× leaner than geth: that would score a no-history node against a with-history one.

State (flat storage)
~228 GiB
Block bodies
~595 GiB
Receipts
~249 GiB
Headers + code
~19 GiB

Full-history nethermind ~1.06 TiB. The minimal-history default drops bodies + receipts → ~250 GiB (state + headers), the smaller of the two no-history nodes (ethrex ~475 GiB) — and a tier geth can't reach.

besu lands in the same band too, at ~1.08 TiB — the same order of magnitude, not an outlier. reth (window-capped at 72h, 47% by block / ~21% gas-weighted) already tracked ~87% of geth's size at that point and projects to ~1.1–1.2 TiB finished. So the four ELs with full post-merge history — geth (1.13), nethermind (~1.06), besu (1.08), reth (~1.1–1.2 projected) — converge on roughly the same footprint. Disk size here is set by a client-agnostic knob (how much post-merge history you retain), not by client efficiency, so it isn't a good axis for picking a winner.

That leaves the axes that actually differ: snap-sync speed and restart-resume stability (both covered below). nethermind is still a good pick — its flat-storage state is genuinely compact, and it's a minority client, so running it improves mainnet client diversity — just not because it's smaller on disk than geth.

The rest of the field didn't produce a comparable finished footprint, each for a specific, documented reason — not a blanket failure:

  • ethrex — synced, ~2h16m, fastest in the field. Its datadir plateaus at ~470–476 GiB: it climbed toward ~465 GiB during post-sync settling (+43 GiB/hr), then growth collapsed ~300× to +0.13 GiB/hr and drifted 470.2 → 475.5 GiB over ~42 hours (confirmed 2026-07-28→31). The earlier ~467 GiB reading was this same plateau caught mid-climb, not evidence of unbounded growth. That doesn't make it a disk winner, though: it lands smaller only because it serves no history at all — a no-history node, not a pruned-comparable one. On a state-only basis it isn't even the smallest: nethermind's state alone is ~226–230 GiB, roughly half ethrex's entire total (not a perfectly controlled comparison — ethrex's total also includes headers and recent blocks, and the two clients use different state encodings).
  • reth — 72h cap at ~21% gas-weighted (47% by block count), ~0.98 TiB partial. Full-sync-only (no snap) — can't reach tip in a practical window, though its trajectory already projects into the converged band above.
  • nimbus_eth1 — 72h cap at ~21.6%, ~40 GB partial. Full-sync-only (no snap). Pruning works (below), but it can't finish in 72h.
  • erigon — deadlocked, no result. Optimistic-sync deadlock against a checkpoint-synced CL (below).

ethrex's no-history design has a concrete cost worth spelling out, since this repo ships an nginx/Caddy RPC-endpoint feature for exposing a node's RPC publicly. Probed live against the running node (2026-07-29), the servable window's back edge is exactly the snap-sync pivot block — eth_getBlockByNumber returns null one block before it and resolves cleanly at and after it, and it never backfills. Current-state calls (balances, current quotes, allowances) work fine, but any block, log, or receipt before the pivot fails outright — effectively all of Ethereum history — which rules out indexer/subgraph backfill, portfolio history, and tax/accounting exports. A geth endpoint with --history.chain postmerge serves that same history; an ethrex endpoint does not, so it isn't a drop-in replacement for a public DeFi-facing RPC.

genesis · block 0merge · 15,537,394snap pivot · 25,634,445head
returns null — pre-pivot blocks/logs/receipts (indexers, portfolio history, tax exports break)served — pivot → head (~4,783 blk at measurement; grows forward, never backfills)state only — last ~128 blk (~25 min)

not to scale — the served window is ~0.02% of the chain (4,783 of 25.6M blocks)

ethrex answers from the block it snapped at, forward — the cutoff is exactly the pivot (probed to single-block precision). Wallet reads at head work fine; anything historical returns null. That's why its ~470 GiB isn't a disk win — it keeps almost no chain.

The speed story: ethrex wins, by a lot — and then loses it on restart

ethrex snap-synced to a fully-validating head in ~2h16m, the fastest in the field by nearly 4×. Fifty peers throughout, one automatic stale-pivot self-heal (~4 min, no intervention), no crash. On paper it's the star.

Two things keep it out of the winners' circle:

  1. The footprint is settled now, and it's not comparable. ethrex prunes nothing, and we watched the datadir climb even at the chain tip with eth_syncing=false (286 → 403 → 416 → ~467 GiB across a single day, ~10 GiB/hr averaged across the 2026-07-06 run, before the +43 → +0.13 GiB/hr settling curve was resolved by the 2026-07-28 steady-state run) — but a follow-up run confirmed that climb was settling, not unbounded: it plateaus at ~470–476 GiB (drifting 470.2 → 475.5 GiB over ~42 hours, 2026-07-28→31). That still doesn't make it a disk winner, because it simultaneously serves almost no history (eth_getBlockByNumber returns null below its snap pivot). So it is neither compact nor a full-history archive — its settled size just isn't rankable against the full-history clients above.
  2. The restart cliff — the next section.

Restart resilience

Cold-sync numbers tell you how a node behaves once, on day one. But operators restart nodes constantly — upgrades, config changes, crashes, host maintenance. “What happens after a restart with a gap?” is a first-class operational question, and it cleanly separates the field into three behaviors:

A running node gets interrupted

what happens next depends on one thing: was it already synced, or still syncing?

Gap after it synced

Catches back up — geth, nethermind

imports missed blocks, keeps datadir — minutes

Starts over — ethrex, head freezes past a ~25-min gap

longer (~1.5–2h) gaps discard state, re-snap from scratch — ~2 h

Interrupted during sync

Wedges — alive but frozen — besu

pivot ages out; answers RPC, writes 0 data — manual rebuild

Why: a full node serves only ~128 recent blocks of state (~25 min); cross that and you can't resume by state.

Scope: the four clients with an observed restart or interruption outcome — geth, nethermind, and ethrex after syncing; besu mid-sync. reth, nimbus-eth1, and erigon never synced far enough to see one. Consensus layer: prysm resumed from its own DB with no re-checkpoint in all four restart tests; the one deliberately timed stop (30 min) was back at sync_distance=0 in ~2m44s.
  1. Graceful resume. The client comes back, imports the blocks it missed during the gap, and keeps its on-disk state. Minutes to catch up, no re-download. This is what makes a client operationally boring, in the good way. We measured this directly for geth: restarted after a ~52-hour gap, it kept its full datadir and caught up purely by sequential block-import (trie-diff application) — never re-snapping — and converged back to the validating tip. That's the exact positive contrast to ethrex's cliff. nethermind's resume is graceful too — see nethermind's resume, bisected, below. reth was never measured for resume — it never reached a synced datadir to restart.
  2. Re-snap cliff. Past a downtime threshold ethrex first stalls with a disconnected head; in the longer measured gaps it discarded its fully-synced state and re-synced from scratch. Only ethrex lands here — and we pinned the onset precisely.
  3. Mid-sync deadlock. If the CL stops driving the engine during an in-progress snap sync, the EL's pivot ages out of the network's servable-state window and the sync wedges irrecoverably — the process stays alive and answers RPC but makes zero progress. besu is the cautionary tale here.

Behaviors 2 and 3 share one root cause: a full node only serves recent world-state (roughly a ~128-block window). Once your head or pivot ages past it, peers can no longer serve the state you need to heal, so you can't resume by state — you're forced to re-pivot. Graceful-resume clients dodge this by importing gap blocks (always available) instead of re-fetching state.

Ethrex
resumed ≤124 blk; stalled at 132 blk (~25 min); ~1.5–2h gaps → full re-snap
Nethermind
resumed every tested gap, 69 → 10,607 blk (12 min → ~35h); largest in 35m09s
Geth
resumed ~15,400 blk (~52h), no re-snap

nethermind's resume, bisected

Measured and then bisected (2026-08-01→03). First an opportunistic catch-up: a CL restart at 13:24:55Z left nethermind 10,607 blocks (~35h of chain) behind the external tip, and it closed the entire gap by ordinary block import in 35m09s (~302 blocks/min) with the datadir intact (1.165 → 1.178 TB, +1.1% — exactly the imported bodies/receipts). Then a controlled stop→wait→start bisection at 12 min / 30 min / 1 h / 4 h gaps (2026-08-02→03): every gap resumed geth-style — ordinary Engine-API block import, no re-pivot, no snap/state-sync, zero crashes. The tell is the state-dir delta: ~1.0–1.3 MiB per imported block, constant across rungs — linear import, the opposite of a re-snap, which would rewrite the whole ~238 GiB state. Resume time scales gently (121s at 12 min → 483s at 4 h → 35m09s at ~35 h), dominated by the CL re-syncing its missed slots rather than by EL import. nethermind has no servable-window cliff — the direct contrast to ethrex's ~128-block cliff below. A separate establish run (2026-07-31) snap-synced nethermind fresh in 1h52m51s (~280 GiB at snap, pivot 25,649,064, zero restarts) — far faster than the ~14.5h Stage-B figure because the pivot was minutes-old and near-tip, and network conditions differ; a second data point under different conditions, not a replacement for the Stage-B number. Artifacts: exp-lab runs exp-a-nethermind-restart-resume-2026-07-31 and exp-a-bisection-2026-08-02.

ethrex's cliff, bisected

After a routine ~1.5–2h restart gap, a fully-synced ethrex (286 GiB, at mainnet head) abandoned its state and began a fresh full snap sync from the current head — datadir collapsing 286 GiB → ~9 GiB → climbing, journal showing SNAP SYNC STARTED from near-genesis, eth_blockNumber at 0x0 throughout. It re-ran the entire ~2h pipeline. The re-snap itself was timed at 2h11m — a full re-run of the cold-sync pipeline, on a datadir that had been fully synced before the gap.

The obvious follow-up: how big a gap actually trips it? We bisected it with controlled stop → wait → start cycles, a live prysm driving forkchoice:

12 min gapresumed cleanly

68 blocks missed

20 min gapresumed cleanly

108 blocks missed

23 min gapresumed cleanly

124 blocks missed

26 min gapstuck — Failed to fetch headers for sync head

132 blocks missed

The cliff edge is ~128 blocks ≈ 24–25 minutes. And the bisection corrected our understanding of the mechanism: the true trigger is header-fetch failure once the gap exceeds the ~128-block servable window — not “state expiry” per se. Just past the edge, ethrex first stalls with a disconnected head (peers won't serve the gap headers); at larger gaps (~1.5–2h) that escalates to the full datadir-collapse re-snap. The stuck head is the onset; the re-snap is where it ends up.

Why it matters: a client that can stop resuming after ~25 minutes and, on longer measured gaps, fall into a ~2h re-snap is genuinely painful to operate. That's a strong candidate explanation for ethrex's ~0% adoption despite best-in-field cold-sync numbers: great benchmark, painful to actually run.

Fairness caveats: observed on ethrex v19.0.0, a young client — this may well improve. The cliff does not change the recorded sync-time result; it's a separate resilience finding presented alongside the cold-sync number rather than folded into it.

besu's mid-sync deadlock

besu's pruned re-run deadlocked twice and was abandoned. The chain:

  1. A stale prysm v7.1.5 (a PeerDAS/data-column-sidecar bug) stalled the CL for ~28h; besu logged Execution engine not called in 120 seconds continuously.
  2. With no forkchoiceUpdated driving it, besu's snap-sync pivot aged out of the servable window — the world-state heal became un-completable.
  3. besu threw IllegalStateException: The pivot block number has not increased, cancelled its snap-sync download (SnapSyncChainDownloader.consumePivotUpdate), and the downloader thread died without restarting. The process stayed alive and still answered eth_blockNumber — but the sync engine was dead and the datadir frozen.
  4. Restarting resumed on the same stale pivot and re-deadlocked identically.

Takeaways: an in-progress besu snap sync is fragile to a prolonged CL outage — a stale CL binary can poison the EL's pivot irrecoverably; and besu answering RPC ≠ besu syncing (judge by disk growth and DB writes rather than RPC liveness). Note the shared root with ethrex's cliff: same ~128-block servable-state window, one hitting mid-sync, the other post-sync-on-restart.

The full-sync-only clients — and a contested flag, settled

reth and nimbus_eth1 have no snap-sync path; they full-sync from genesis. Both hit the 72h cap far from tip (reth ~21% gas-weighted (47% by block count), ~0.98 TiB; nimbus_eth1 ~21.6% by block, ~40 GB). This is a client-design limitation for our snap-to-tip bar, not a failure — it would be unfair to rank a from-genesis full sync against a snap sync on either time or disk. reth in particular is widely and successfully run elsewhere.

nimbus_eth1 did settle one open question for us. Its config carries prune = true, and whether that flag actually does anything was genuinely contested: the binary's --help claims it prunes expired bodies and receipts, while the online docs say pre-merge history needs a separate era1 export — i.e. that the flag is effectively inert. We'd flagged it “unverified.” The 72-hour run answered it directly: the journal logged continuous online pruning (Pruning history … pruned=N) throughout block import. So the flag is not inert — nimbus_eth1 prunes history online as it syncs. (Whether it reaches full pre-merge completeness versus an era1 import stays untestable here, since the node never reached tip — but the “does it do anything?” question is now a clean yes.) As a bonus data point, that run stayed up 72 hours with zero restarts: stable, just slow by design.

erigon was the one hard deadlock: erigon3's OtterSync plus a checkpoint-synced prysm wedged in a mutual wait — erigon waiting for the CL to finalize, the CL waiting for erigon to execute. Raising the CL CPU cap 200%→600% pushed it ~5k blocks further, then it re-froze — a ~1.21 TiB frozen partial, not a synced datadir. The single no-sync of the EL sweep.

Mainnet share half-predicts syncability — and ethrex breaks it

A tempting story going in was “mainnet share predicts syncability” — the low/zero-share clients are exactly the ones that struggle. The data only half-supports it, and ethrex breaks it outright: a ~0%-share minimalist client synced fastest in the entire field. Several minority clients did struggle (erigon's deadlock; reth and nimbus_eth1 too slow by design), but the real predictor is snap-sync availability plus client robustness, not market share per se. ethrex has snap sync and clock-based stale-pivot self-healing during the initial sync — and it excelled at cold sync. Its adoption gap is far better explained by the restart cliff than by any sync deficiency.

The consensus layer looks solved — on the axes we measured

We ran the five CLs — lighthouse, lodestar, grandine, teku, nimbus — against a constant anchor EL, and then repeated it twice more against different anchor ELs to test the EL/CL decoupling claim directly. Every CL checkpoint-synced to a fully-validating head in minutes — ~22–23 minutes on the ethrex anchor, ~6–9 minutes on the geth anchor (whose footprints are in the CL scorecard above), and ~7–10 minutes on the nethermind anchor (lodestar's first attempt on that anchor recorded ~76 minutes, but the anchor EL was still importing a ~2-day block gap at the time; the clean re-read is ~7m36s and is what the scorecard uses), config_optimal=yes, zero crashes (teku and grandine each needed a caveat across the sweeps — a JVM heap-sizing fix, a harness artifact, and, on the nethermind anchor, a watchdog false positive on teku's anchor-health verdict — not client faults). Sync time is effectively tied within each anchor, so footprint is the differentiator.

The tiers reproduced across all three anchor ELs — a lightweight pair (lodestar, lighthouse), a mid pair (teku, grandine), and nimbus alone at the heavy end — with two swaps: lodestar↔lighthouse within the lightweight pair (ethrex vs geth), and teku itself across two runs on one anchor (~667 vs ~848 MiB). Three different EL anchors, the same three tiers, no identical total order: EL/CL decoupling, supported empirically — which retroactively validates holding CL=prysm constant for the whole EL scorecard.

The punchline: on the CL side, all five are operationally effective — none failed, and the choice comes down to footprint and preference (lighthouse is the lean, safe default). Operational risk in an Ethereum node lives in the EL layer, not the CL layer. One scope limit: the five CLs were swept for checkpoint-sync and footprint, not for restart-resume. The only CL we restart-tested is prysm, the constant anchor, and it resumed cleanly from its own DB — that earns “prysm resumes cleanly,” not “the CL layer survives anything.”

Recommendations

  • Default: geth. Largest ecosystem, most documentation, the cleanest snap sync (~8.5h), and it resumes gracefully across restarts. Its disk footprint (~1.13 TiB) is on par with the other ELs that carry full post-merge history — not a downside unique to geth. If you don't have a specific reason to run something else, run this.
  • Diversity pick: nethermind. Compact flat-storage state, a minority-client diversity bonus, and restart-resume that is now measured and bisected, not just assumed (2026-08-01→03: every gap from 12 min to ~35h resumed by plain block import, no re-snap, no cliff — see “Restart resilience” above). On disk it's on par with geth (~1.06 vs ~1.13 TiB) once full post-merge history is counted — not the space-saver its snap-sync-tip snapshot (~251 GiB) suggested. Costs a bit more sync time (~14.5h vs geth's ~8.5h).
  • Consensus client: lighthouse as the lean default; any of the five is operationally fine — pick on footprint and familiarity.
  • Watch, don't yet deploy: ethrex. Fascinating and fastest, but the ~25-minute restart cliff makes it operationally costly today. Its footprint is now settled too — a ~470–476 GiB plateau — but that's not a disk win: it's a no-history node, and running its RPC in place of a full-history endpoint (this repo's nginx/Caddy feature) will silently fail on anything historical. Fast-moving client — v19.0.0 at first sync, v22.0.0 by this steady-state measurement — worth revisiting.
  • Enterprise with care: besu. It syncs, but its snap sync is fragile to CL outages; handle upgrades and CL health deliberately.
  • Know the design limits: reth and nimbus_eth1 are full-sync-only — excellent clients, but plan for a long initial sync rather than snap-to-tip. Avoid erigon3 + a checkpoint-synced CL until the optimistic-sync deadlock is resolved.

The most useful thing this bake-off surfaced isn't a single winner — it's that the number that matters to a running operator is often not the one on the benchmark chart. Cold-sync time and disk footprint are easy to measure and easy to publish. Restart resilience is neither — and it's the best candidate explanation we found for why the fastest-syncing client in this field is also the one nobody runs.

Read next

Source docs on GitHub