Captured on July 2, 2026 while evaluating Phase 8 Tier-3 state-diff resync. This note records the measured behavior of:
debug_traceBlockByNumberwith GethprestateTracerdiffMode: true- batched
eth_getStorageAtpoint reads for known slots - gzip-compressed HTTPS responses for large trace payloads
The core takeaway is deliberately split into three axes:
| Axis | Result |
|---|---|
| Alchemy compute units | Trace wins once it replaces 3 or more point storage reads in the same block. |
| Latency without compression | Storage batches stayed faster through the clean measurable range on the public endpoint tested. |
| Latency with gzip on Alchemy | Gzip substantially reduced one large trace response's transfer time. |
Alchemy's published CU table lists:
eth_getStorageAt: 20 CUeth_getProof: 20 CUdebug_traceBlockByNumber: 40 CU
That means the CU break-even is two point reads:
| Storage slots repaired from one block | Point-read CU | Trace CU | Trace CU result |
|---|---|---|---|
| 1 | 20 | 40 | 2x more expensive |
| 2 | 40 | 40 | break-even |
| 3 | 60 | 40 | 33% cheaper |
| 10 | 200 | 40 | 5x cheaper |
| 100 | 2000 | 40 | 50x cheaper |
This is a billing/throughput result, not a latency result. Trace responses are large JSON payloads and can still arrive later than a small storage batch.
Sources:
- Alchemy CU table: https://www.alchemy.com/docs/reference/compute-unit-costs
- Alchemy debug endpoint: https://www.alchemy.com/docs/node/debug-api/debug-api-endpoints/debug-trace-block-by-number
- Geth tracer behavior: https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers
- Pool: Uniswap V3 USDC/WETH 5 bps
0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640 - Trigger: live
Swap(address,address,int256,int256,uint160,uint128,int24)subscription plus historical blocks containing Swap logs. - Trace call:
debug_traceBlockByNumber(block, {"tracer":"prestateTracer","tracerConfig":{"diffMode":true}}) - Storage calls:
one JSON-RPC batch of
eth_getStorageAt(pool, slot, block)for slots0..N-1. - Endpoint for the scaling run: MEW public Ethereum RPC, which supports both
debug_traceBlockByNumberand JSON-RPC storage batches.
This benchmark intentionally measures end-to-end RPC response latency from one client process. It does not isolate node execution time from network transfer time.
| Slots | Trace latency | Storage batch latency | Winner |
|---|---|---|---|
| 1 | 455 ms | 111 ms | storage |
| 8 | 418 ms | 73 ms | storage |
| 32 | 282 ms | 35 ms | storage |
| 128 | 610 ms | invalid | storage batch had 29 rate-limit errors |
Clean successful median latencies:
| Slots | Trace median | Storage median | Trace wins | Storage wins |
|---|---|---|---|---|
| 1 | 480 ms | 34 ms | 0 | 4 |
| 2 | 352 ms | 55 ms | 1 | 3 |
| 4 | 476 ms | 34 ms | 0 | 4 |
| 8 | 305 ms | 48 ms | 0 | 4 |
| 16 | 645 ms | 31 ms | 0 | 4 |
| 32 | 277 ms | 47 ms | 0 | 4 |
| 64 | 332 ms | 92 ms | 1 | 3 |
| 80 | 182 ms | 42 ms | 0 | 4 |
| 96 | 486 ms | 233 ms | 0 | 2 |
No reliable latency crossover was observed before the endpoint's clean batch
limit. The same public endpoint started returning per-item rate-limit errors near
100 eth_getStorageAt requests per second, even when sent as one JSON-RPC
batch. A noisy linear interpolation suggested a possible crossover around 260
slots, but that is outside the clean measurable region and should not be treated
as a production threshold.
Interpretation:
- For small and medium known slot sets, batched storage reads are latency-faster.
- Trace can still be preferable when CU budget, completeness, or avoiding many point reads matters more than wall-clock latency.
- Provider behavior matters. Public RPC endpoints differ materially in debug namespace support, batch limits, response caching, and rate limits.
- Endpoint: Alchemy Ethereum mainnet HTTPS.
- Block:
25444665(0x1844139). - Payload: same
debug_traceBlockByNumberprestate diff call. - Client: raw HTTPS with automatic decompression disabled, so wire bytes, decompression time, and JSON parse time were measured separately.
- Comparison:
- Standard:
Accept-Encoding: identity - Gzip:
Accept-Encoding: gzip
- Standard:
| Mode | Wire bytes | Full response received | Parsed and ready |
|---|---|---|---|
Standard HTTPS (identity) |
4.48 MB | 664 ms | 671 ms |
Gzip HTTPS (gzip) |
1.03 MB | 268 ms | 286 ms |
Observed improvement:
- Wire payload reduction: 76.9%
- Receive-time improvement: 397 ms
- Parsed-and-ready improvement: 385 ms
- End-to-end parsed latency improvement: about 57%
- Local gzip decompression cost: 11 ms
A separate concurrent sanity check on another block also reduced wire bytes by 78.7%, but gzip arrived slower in that single paired sample. Treat compression as a strong payload-size win and a likely latency win for large traces, but keep latency thresholds empirical per provider.
- Always request gzip for debug/trace RPC over HTTPS when the provider supports it. The payload is large, repetitive JSON and compresses well.
- Do not replace small known-slot repairs with trace solely for latency. Storage batches were faster up to the clean measured range.
- Keep the trace source as an accelerator with fallback, not a hard dependency.
debug/tracenamespaces are commonly gated or disabled. - Make the trace decision adaptive:
- Use point reads for low slot counts on latency-sensitive hot paths.
- Use trace when expected unresolved targets per block are high enough, when CU budget dominates, or when whole-block completeness is valuable.
- Continue falling back to point reads for cold targets absent from the trace diff.
- Separate thresholds by objective:
- CU threshold on Alchemy: trace breaks even at two point reads and wins at three or more.
- Latency threshold: not established by the public-endpoint run; after gzip, it should be measured directly on the target Alchemy plan and region.
Run the same scaling harness directly against Alchemy with:
- gzip enabled for trace
- the same Alchemy endpoint for storage batches
- slot counts past 100 if the plan's rate limits allow it
- at least 20 samples per slot count bucket
- separate reporting for transfer time, decompression time, JSON parse time, and provider errors
Until that exists, the implementation should expose configuration rather than hard-code a latency threshold.