← writing

Feb 2026

What I learned from performance work in reth

Notes from submitting perf PRs to Paradigm's Ethereum execution client — sparse trie optimization, engine tree improvements, and navigating a 500k LOC Rust codebase.

reth is the fastest Ethereum execution client. It's also ~500k lines of Rust, moves at a pace that would bury most contributors, and has maintainers who will close your PR if the benchmark doesn't justify the complexity. Contributing to it teaches you things you can't learn from books.

Here are the patterns I've picked up from getting performance work merged.

Find the hot path first, not the interesting code

The natural instinct is to find the most architecturally interesting part of the codebase and dig in. That's the wrong approach for perf contributions. Start with a profiler.

For reth, perf + flamegraph on a syncing node shows you where time actually goes. The answer is usually uninteresting-looking code — tight loops in trie computation, serialization/deserialization paths, lock contention in the engine tree. Not the elegant state machine code you'd want to read.

My merged PR #22116 (perf(reth-engine-tree): sparse trie bulk move new storage update) came from noticing that sparse trie updates were doing per-key operations that could be batched. The code wasn't complex — it was a loop with an inner allocation that could be hoisted. The gain was measurable because it sat directly on the critical path during block processing.

Understand what "perf" means in this context

reth's performance is measured in two ways: sync speed (how fast you can get to chain tip from genesis) and block processing latency (how fast you can process a new block after receiving it from peers).

These have different profiles. Sync speed is throughput-bound — you care about total CPU and I/O efficiency over millions of blocks. Block processing latency is tail-latency-bound — you care about the worst case on a heavy block, because that determines whether you keep up with the chain.

Changes that help sync speed sometimes hurt block latency and vice versa. The maintainers are acutely aware of this. If your benchmark only covers one, they'll ask about the other.

Sparse tries are where the interesting work is

The merkle patricia trie is Ethereum's state commitment structure — every block update requires recomputing roots from thousands of changed leaves. reth's sparse trie is an optimization: instead of loading the full trie, you only load the nodes you actually need for the current update set.

Getting this right is subtle. The sparse trie needs to maintain enough structure to produce valid proofs, but not so much that it becomes equivalent to the full trie under heavy blocks. The balance point shifts depending on the update pattern.

Most of my reth work has been in this area. It's where the perf leverage is highest, and where the correctness constraints are tightest — a good combination if you want work that actually gets reviewed seriously.

Don't submit without a benchmark

Every perf PR needs a benchmark. Not a microbenchmark of the changed function — a benchmark of a realistic workload that exercises the changed path. For reth this usually means running reth import over a range of mainnet blocks and comparing cycle counts or wall time.

The benchmark has to be reproducible. If you can't pin the CPU governor and disable turbo boost, your numbers will have enough variance to be dismissed. I run benchmarks on a dedicated EC2 metal instance specifically to avoid this.

A PR without a benchmark is a PR the maintainers have to do extra work to evaluate. Most will just close it and ask you to come back with numbers.