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”

Restart behavior mattered more than sync speed or disk size. We tested every execution and consensus client this project supports, one mainnet sync at a time, recording both numbers. The fastest client has almost no real-world adoption.

Companion post: for agent orchestration, the test harness, and methodology, see How We Ran a Six-Week Ethereum Client Bake-Off With Claude.

At a glance: when & how

When

23-day measurement phase, 2026-06-22 → 2026-07-14; steady-state and restart-resume follow-ups through 2026-08-03.

Where

One shared semi-production host — not a live validator. MEV disabled, no validator keys.

Scope

Seven execution clients and five consensus clients, through the same mainnet sync — one client at a time, a 72-hour cap on each.

What we recorded

Two numbers: final disk footprint and cold-sync duration (syncing from scratch). Footprint is the end-of-run data directory, not the mid-sync peak. Capped runs use the last du disk-usage sample before the cap.

TL;DR

Disk: the field converges, ~1.0–1.2 TiB

Execution-layer (EL) clients with full post-merge history use similar disk space: geth 1.13 TiB, nethermind ~1.06 TiB, besu 1.08 TiB. Retention settings, not client efficiency, determine disk size, so it is a poor way to pick a winner.

Speed winner — ethrex, ~2h16m

Ethrex finished first; geth followed at ~8h28m.

The twist — ethrex's restart-resync cliff

Ethrex resumed through 23 minutes / 124 blocks but stalled at 26 minutes / 132 blocks. Measured 1.5–2-hour gaps forced a fresh snap sync (downloading state again, ~2h). This restart cost is a strong candidate explanation for its near-zero mainnet adoption.

The CL layer looks solved — on the axes we measured

All five consensus-layer (CL) clients checkpoint-synced to a validating head in minutes on all three EL anchors: geth ~6–9 min, nethermind ~7–10 min, ethrex ~22–23 min. Four runs needed fixes or re-reads, detailed below; none exposed a client fault. Disk footprint separates them.

Cold-sync time, at a glance

Ethrex synced fastest. Only completed execution-client syncs are comparable; reth and nimbus_eth1 hit the 72-hour cap, and erigon deadlocked.

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

Full-history clients converge at ~1.0–1.2 TiB; retention settings determine size. Solid bars show nethermind, besu, and geth. Hatched bars show different outcomes:

  • Nimbus-eth1: ~40 GB at only ~21% synced; reth: ~0.98 TiB at the 72h cap, projected to finish in the solid bars' band.
  • Erigon: frozen partial data from its no-sync deadlock.
  • Ethrex: settled at ~470–476 GiB, serving no history. Its smaller size reflects retention, not greater efficiency; it cannot be ranked against pruned full-history nodes.

A short hatched bar is not a disk win.

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

Four execution clients synced. Every 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

All five consensus clients synced on all three fixed EL anchors: ethrex, then geth, then nethermind. Repeating the sweep tested whether CL results depend on the paired EL.

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
~835 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

Footprint tracks time following the chain, rather than the paired EL. We sampled the geth and nethermind runs minutes after checkpoint-sync, on fresher data directories (datadirs).

Three footprint tiers held across anchors: lightweight (lodestar, lighthouse), middle (grandine, teku), and heavy (nimbus). Sampling time affects order within tiers; the consensus-layer section explains two instabilities.

Additional run details

These collapsed details add peer counts, configuration checks, re-runs, and per-candidate notes to the scorecards.

Show all 10 per-candidate rows — peers, config-optimality, re-runs, notes

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

CL 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
2

Second cross-anchor confirmation; teku re-measured (~667→~835 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

Comparable results need controlled pairing and verified settings. Each client ran alone; the campaign card defines the 72-hour cap and end-of-run disk measurement. We held the consensus client constant and checked the running configuration.

Prysm stayed constant for the EL scorecard. The Engine API connects EL and CL; EL footprint and sync time are independent of the CL. The CL matrix confirmed this. Prysm used ~0.65–1.68 GB in bounded runs, reaching ~12.5 GB during reth's full 72h cap. That remains negligible beside an EL's hundreds of gigabytes.

The honesty mechanism

Early in the campaign we corrupted our own results by recording footprints before checking disk settings. A benchmark of our misconfiguration is worse than no benchmark. The harness now checks generated, running settings and stamps every row config_optimal=yes|no. It rejects misconfigured footprints. We needed six gate fixes across three review rounds before trusting it.

Every comparable footprint on this page comes from a synced run with verified disk settings. The harness config_optimal gate verifies most; besu needed a manual sync-flag audit. Its harness token targets a prune lever the ranked run deliberately omitted. Capped, no-sync, and pruned-only runs are labelled and excluded.

Live checks also settled configuration disputes, including nimbus_eth1's history-pruning flag below. The binary's --help and the online docs contradicted each other. See how we tested this with Claude for the full harness engineering process.

The disk story

Matched history settings put nethermind and geth on par. Nethermind's synced-tip snapshot was ~251 GiB, below geth's ~1.13 TiB. FastBlocks had yet to backfill older post-merge block bodies and receipts.

Re-measured 2026-08-01, its steady-state datadir is ~1.06 TiB (~1,088 GiB): ~226–230 GiB of state in compact Halite/Paprika flat storage, ~843 GiB of post-merge bodies and receipts, and ~19 GiB of headers and code. Geth keeps the same history under --history.chain postmerge.

Since 2026-08-03, the shipped default drops that history:

  • Default is minimal-history (NETHERMIND_FULL_HISTORY=false) — a fresh sync drops the post-merge bodies and receipts and holds at ~250–280 GiB (state only), with no backfill.
  • The cost is served history: pre-sync blocks return null, like ethrex. It is the same retention lever turned down — not a client that is “smaller.”
  • To serve a public RPC, set NETHERMIND_FULL_HISTORY=true on a fresh or rebuilt datadir; converting an existing minimal datadir needs a rebuild.

The ~1.06 TiB figure and the chart below are that full-history opt-in.

Nethermind is the smallest measured no-history staking node:~250–280 GiB against ethrex's ~470 GiB. Geth has no clean setting to drop post-merge history and cannot reach that floor. This is the one place its compact state engine is a genuine disk win, at the cost of serving no history. Calling it 4× leaner than geth would compare different retention settings.

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 uses ~1.08 TiB. Reth already used ~87% of geth's size at its 72h cap (47% by block / ~21% gas-weighted), projecting to ~1.1–1.2 TiB finished. Those two join geth (1.13) and nethermind (~1.06) in the band retaining full post-merge history.

Snap-sync speed and restart-resume stability separate the clients, as detailed below. Nethermind remains a good choice for its compact flat-storage state and minority-client diversity, just not because it is smaller on disk than geth.

The remaining footprints are not comparable finished results:

  • ethrex — synced, ~2h16m, fastest in the field. Its datadir plateaus at ~470–476 GiB. Post-sync growth toward ~465 GiB (+43 GiB/hr) slowed ~300× to +0.13 GiB/hr. It drifted 470.2 → 475.5 GiB over ~42 hours (confirmed 2026-07-28→31). The earlier ~467 GiB sample caught the climb, not unbounded growth. It lands smaller only because it serves no history at all. On a state-only basis it is not even the smallest: nethermind's ~226–230 GiB state is roughly half ethrex's total. This comparison is imperfect: ethrex's total includes headers and recent blocks, and state encodings differ.
  • 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). Online pruning is logged (below), but it can't finish in 72h.
  • erigon — deadlocked, no result. Optimistic-sync deadlock against a checkpoint-synced CL (below).

Ethrex is not a drop-in replacement for a public DeFi-facing RPC, including this repo's nginx/Caddy endpoint feature. A live probe (2026-07-29) placed its history cutoff exactly at the snap-sync pivot, the block chosen to start syncing: eth_getBlockByNumber returns null one block before it and resolves at and after it. Ethrex never backfills older history.

  • Current balances, quotes, and allowances work.
  • Blocks, logs, and receipts before the pivot fail: effectively all Ethereum history. Indexer/subgraph backfill, portfolio history, and tax/accounting exports cannot work.
  • Geth with --history.chain postmerge serves that history.
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 snap-synced to a fully-validating head in ~2h16m, the fastest in the field by nearly 4×. It kept fifty peers throughout, automatically healed one stale pivot in ~4 min, and never crashed.

Two things keep it off the recommended list:

  1. Disk size reflects missing history. Ethrex prunes nothing. Even at tip with eth_syncing=false, its datadir grew 286 → 403 → 416 → ~467 GiB on 2026-07-06 before settling at ~470–476 GiB. It serves almost no history: the pivot cutoff above prevents comparison with full-history clients. It is neither compact nor a full-history archive.
  2. The restart cliff — the next section.

Restart resilience

Geth and nethermind resumed cleanly; ethrex stalled or restarted; besu deadlocked mid-sync. Upgrades, config changes, crashes, and host maintenance make restart behavior an everyday concern. Cold-sync numbers describe only day one.

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 imports missed blocks and keeps its on-disk state, catching up in minutes without re-downloading. Geth bridged a ~52-hour gap by sequential block import (applying trie differences), kept its full datadir, and reached the validating tip without re-snapping. Nethermind also resumed gracefully; its bisection follows below. Reth never reached a synced datadir, so we could not measure resume.
  2. Re-snap cliff. Past a downtime threshold, ethrex stalls with a disconnected head. Longer measured gaps caused it to discard synced state and start again. Only ethrex showed this behavior; the bisection pins its onset.
  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.

Both failures involve a limited serving window. Full nodes serve only recent world-state (roughly ~128 blocks). When a head or pivot falls behind it, peers cannot supply the state needed to heal. The client must choose a new pivot. Graceful-resume clients import gap blocks, which remain available.

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

Nethermind resumed every tested gap without re-snapping (2026-08-01→03). In an opportunistic catch-up, a CL restart at 13:24:55Z left it 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).

Controlled stop→wait→start tests 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.

Separate fresh-sync measurement: 2026-07-31

A separate establish run (2026-07-31) snap-synced nethermind fresh in 1h52m51s (~280 GiB at snap, pivot 25,649,064, zero restarts). This was far faster than the ~14.5h Stage-B figure because the pivot was minutes-old and near-tip, and network conditions differed. It is a second data point, not a replacement for the Stage-B number.

ethrex's cliff, bisected

A ~1.5–2h restart gap forced fully-synced ethrex to start again. At mainnet head with 286 GiB, it abandoned state for a fresh snap sync from the current head. The datadir collapsed 286 GiB → ~9 GiB, then climbed. The journal showed SNAP SYNC STARTED from near-genesis, eth_blockNumber at 0x0 throughout. Re-running the entire ~2h cold-sync pipeline took 2h11m.

Controlled stop → wait → start cycles narrowed the threshold, with live Prysm driving forkchoice (selecting the chain head):

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. Bisection identified header-fetch failure beyond the ~128-block serving window as the trigger, rather than “state expiry” itself. Just past the edge, ethrex first stalls with a disconnected head: peers will not serve the gap headers. The stuck head is the onset; larger gaps (~1.5–2h) end in a full datadir-collapse re-snap.

Why it matters: a ~25-minute restart cliff and ~2h re-snaps after longer gaps make operation costly. This is a strong candidate explanation for ethrex's ~0% adoption despite its fastest cold sync.

Fairness caveats: we observed this on ethrex v19.0.0, a young client that may improve. Restart resilience is separate from the recorded cold-sync result; the cliff does not change that time.

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.

Three 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.
  • besu answering RPC ≠ besu syncing — judge by disk growth and DB writes, not RPC liveness.
  • Same root cause as ethrex's cliff — the ~128-block servable-state window, one client hitting it mid-sync, the other post-sync on restart.

Full-sync-only clients

Reth and nimbus_eth1 full-sync from genesis; neither offers snap sync. Both hit 72h far from tip: reth ~21% gas-weighted (47% by block), ~0.98 TiB; nimbus_eth1 ~21.6% by block, ~40 GB. Their design misses our snap-to-tip bar, not a correctness test. Ranking them against snap sync on time or disk would be unfair; reth runs successfully and widely elsewhere.

Nimbus_eth1's pruning flag was disputed. Its config sets prune = true. The binary's --help says it prunes expired bodies and receipts. Online docs require a separate era1 export for pre-merge history, implying the flag is inert; we had marked it “unverified.”

Throughout the 72-hour import, the journal logged continuous online pruning: Pruning history … pruned=N. The flag is not inert. Full pre-merge completeness versus era1 import remains untestable here: the node never reached tip. It ran 72 hours with zero restarts: stable, but slow by design.

Erigon was the EL sweep's only no-sync result. Erigon3's OtterSync and checkpoint-synced Prysm deadlocked in a mutual wait. Raising CL CPU from 200%→600% advanced ~5k blocks before it re-froze, leaving ~1.21 TiB of frozen partial data, not a synced datadir.

Erigon3 · OtterSync

Waits for CL finalization

Checkpoint-synced Prysm

Waits for EL execution

Neither side issued the forkchoiceUpdated call needed to break the wait.

Distribution as predictor

Snap-sync availability and resilience predict syncability better than mainnet share. The data only partly supports our initial “mainnet share predicts syncability” hypothesis. Erigon deadlocked; reth and nimbus_eth1 were slow by design. But minimalist ethrex, at ~0% share, synced fastest. Its snap sync and clock-based stale-pivot self-healing excelled at cold sync. Its adoption gap is far better explained by the restart cliff than by any sync deficiency.

Consensus layer

All five CLs reached a fully-validating head in minutes on every anchor. We swept lighthouse, lodestar, grandine, teku, and nimbus against one fixed EL, then repeated on two more to test EL/CL independence:

Geth anchor~6–9 min
Nethermind anchor~7–10 min
Ethrex anchor~22–23 min
Shared 0–23 minute scale; darker segments mark each range. Uses lodestar's clean nethermind re-read.

Ethrex was slowest, still well under an hour. Footprints are in the CL scorecard above; lodestar's slow first nethermind attempt was an anchor artifact, detailed below.

Every run logged config_optimal=yes with zero crashes. Four runs needed caveats, detailed below; none exposed a client fault. Sync times are effectively tied within each anchor, leaving footprint as the differentiator.

not a client fault

Teku (ethrex anchor) — First attempt JVM-OOM-starved the shared host (64 min sync, briefly blipped the anchor); re-run with a larger JVM heap synced clean in 22 min.

not a client fault

Grandine (ethrex anchor) — First attempt crashed when du hit a datadir file that vanished mid-snapshot (a harness pipefail bug, since fixed); the re-run synced clean.

not a client fault

Lodestar (nethermind anchor) — First run took ~76 min — not a lodestar property; the anchor EL was still importing an unrelated 2-day gap at the time. Clean re-read: ~7m36s.

not a client fault

Teku (nethermind anchor) — Watchdog reported a false anchor-health miss on both runs, latched during teku's JVM warm-up; the anchor was healthy when each measurement was actually taken.

Three tiers held, with two order instabilities. Lodestar↔lighthouse swapped within the lightweight pair between ethrex and geth. Teku's re-read (~667 vs ~835 MiB) crossed grandine and back on one anchor. Grandine < teku held on all three anchors: the second instability is measurement noise, not a real swap. Stable tiers without an identical total order support EL/CL independence and holding CL=prysm constant for the EL scorecard.

All five CLs worked on the measured axes. Choose by footprint and preference; lighthouse is the lean, safe default. Operational risk in an Ethereum node lives in the EL layer, not the CL layer.

Scope matters: we swept the five CLs for checkpoint-sync and footprint, not restart-resume. Only the constant Prysm anchor had restart tests. Its clean resume from its own DB supports “prysm resumes cleanly,” not “the CL layer survives anything.”

Recommendations

Choose geth by default or nethermind for diversity. Every EL's campaign issues and verdict follow:

GethRecommended

None found. Cleanest snap sync (~8.5h) and the only EL measured resuming a multi-day (~52h) gap without a re-snap.

NethermindRecommended

One installer bug, fixed: P2P defaulted to the loopback address, so the first run peered with 0 nodes for 13.3h before anyone noticed. Every gap tested after the fix resumed cleanly, up to ~35h.

BesuCaution

Synced cleanly once, but a pruned re-run deadlocked twice: if the CL stalls long enough mid-sync, besu's snap pivot ages out and the sync engine dies silently (the process keeps answering RPC).

EthrexCaution

Fastest cold sync in the field (~2h16m), but a restart gap past ~25 minutes stalls, and a gap of ~1.5–2h triggers a full re-snap from scratch. Serves no history before its sync pivot — not a drop-in public RPC.

RethFull-sync-only

No snap-sync path. Never reached tip inside the 72h cap on this host (47% by block count when capped) — a slow bootstrap, not a broken client.

Nimbus-eth1Full-sync-only

Same limitation as reth: no snap-sync path. Ran a full 72 hours with zero restarts but reached only ~21.6% of tip.

ErigonAvoid for now

OtterSync deadlocked against a checkpoint-synced consensus client — each side waited on the other and neither issued the forkchoiceUpdated call that would break the tie. Never produced a synced datadir in this campaign, even after the consensus client's CPU cap was raised 3x.

The detail behind each verdict:

  • Default: geth. Largest ecosystem, most documentation, cleanest snap sync (~8.5h), and graceful restarts. Its ~1.13 TiB footprint matches other ELs retaining full post-merge history, so it is not a downside unique to geth. Run it unless you have a specific reason to choose another client.
  • Diversity pick: nethermind. Compact flat-storage state and minority-client diversity, with measured restart resilience: every 12 min–~35h gap resumed by block import (2026-08-01→03), with no re-snap or cliff. Full-history disk is comparable to geth (~1.06 vs ~1.13 TiB); the ~251 GiB snap-tip snapshot omitted backfill. Sync takes longer (~14.5h vs ~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. Fastest cold sync, but its ~25-minute restart cliff makes it operationally costly today. Its settled ~470–476 GiB plateau serves no history; using it in place of a full-history nginx/Caddy RPC will silently fail on anything historical. Revisit this fast-moving client: v19.0.0 at first sync, v22.0.0 at the steady-state measurement.
  • 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.

Bottom line

Run geth by default or nethermind for diversity. Both synced and resumed cleanly, with similar full-history disk footprints.Ethrex's ~25-minute restart cliff and besu's vulnerability to a stalled consensus client need caution. Restart behavior reveals risks a cold-sync chart misses. It is the strongest candidate explanation for why almost nobody runs the fastest client.

Read next

Source docs on GitHub