Skip to content

Saving storage — fitting an elephant into a box

UMNode's default is a compact archive: every block and the whole state history, each stored once — on Sepolia about 665 GB — with a block's receipts and traces re-executed when they are asked for. --fast-full-archive adds what makes those requests lookups — every block's receipts (the receipts stream), its call traces (the trace cache) and its transactions' senders — taken whole from the segment archive where it publishes them, for about 1.9 TB. Every part of either can be shrunk or dropped, and most of what is dropped can come back from the archive when it is wanted again — and a node that needs only some history can hold just those block ranges, with their caches, and shift through the chain a range at a time (Ranges from the archive, nothing executed). This page lists what takes the space, the knobs, and the strategies that combine them.

What takes the space

A full Sepolia archive node at block 11.9M (October 2026, 1.13 billion transactions), every store at its default codec:

Store Where Size Kept by default Serves
Block segments <block-db>/segments 450 GB all blocks, transactions, the block-by-hash/tx-by-hash lookups
Hash index <block-db>/index 20 GB on eth_getBlockByHash, eth_getTransactionByHash, receipts and traces by hash
Flat state <state-db>/flat 66 GB always, with --execute eth_call, the state getters at the head
State history <state-db>/changesets + hist 88 + 41 GB all state reads and eth_call at past blocks, receipts and debug_trace* by re-execution, reorg unwind
Receipts stream (--receipts store) <state-db>/receipts (<block-db>/receipts without --execute) ≈ 85 GB — (--fast-full-archive) receipts and logs without re-executing (eth_getLogs scans these)
Trace cache (--trace-cache) <block-db>/served/traces ≈ 1.1 TB — (--fast-full-archive) debug_trace* without re-execution
Senders (--senders-cache) <block-db>/served/senders ≈ 23 GB — (--fast-full-archive) blocks and receipts with their senders, none recovered

The last three are derived — the receipts are written by execution (or taken from the archive, checked against the headers), the traces re-executed from the state history, the senders recovered from the blocks' signatures — so dropping them costs only serving speed: receipts and traces are re-executed per request instead, while the state history reaches their blocks. The trace cache is large because it holds what is sent: a trace carries every call's input and output. Its size is measured on 1,978 sample blocks spread over every segment of the chain, checked against eleven sealed segments (within 0.1%); the last million blocks hold about 100 GB of traces, the last 100,000 about 7 GB.

A store filled before v0.2.148 may also hold receipts inline in its block segments (≈ 80 GB on Sepolia) and the former receipts cache (<block-db>/served/receipts, ≈ 270 GB): the inline receipts are still served, neither grows, and the cache's directory — which the node reports at start — is yours to delete.

The knobs

Retention: keep a window, not everything

Flag Shrinks What you give up Comes back from the archive
--block-history N blocks (and their traces and senders) below the window blocks, receipts and traces older than N yes — raise N
--extra-archive-blocks A:B — (keeps ranges outside the window) yes
--state-history N changesets and the history index below the window state reads, eth_call and on-demand traces at older blocks yes — raise N
--receipts-history N the receipts stream (--receipts store) eth_getLogs below the floor (point lookups re-execute) yes — raise N
--trace-cache-history N the trace cache below the window instant traces of older blocks (they re-execute while the state history reaches them) yes — the archive's traces/

A window is counted from the head; whole 50,000-block segments go at a time, so the effective window is up to a segment larger than asked.

The trace cache and the senders, when on, keep everything, and follow the blocks the node keeps: with --block-history N they hold the records of the window, with --extra-archive-blocks those of each range as well, and of no other block, and a range added later gets its records traced, recovered or downloaded. A cache window (--trace-cache-history) narrows the head window only; the ranges, asked for by name, keep their records.

Pruning waits for what is built from the blocks

A block the node no longer keeps takes its cached traces and senders with it — so pruning waits for them to be built first: a block segment stays, whatever --block-history says, until the trace cache, the senders and (with --receipts none and --backup) the receipts builder have each sealed their segment of it. A cache that falls behind execution — tracing 2016's DoS-era blocks on mainnet runs at 6–37 blocks/s against 190 executed — therefore keeps the blocks it still has to trace, and the trace cache keeps the state history it traces them from too: --state-history prunes no higher than its lowest segment not built yet. The node then builds upward, lowest segment first, so the holds go one segment after another. The floor is always a segment start, so the segment holding it stays whole.

How far the wait goes:

  • a segment the archive publishes holds nothing — it is downloaded whole;
  • a segment the state history no longer reaches cannot be traced here, nor one whose first blocks' BLOCKHASH blocks were pruned before they were traced (they would read zero hashes: they are not traced into the cache at all), nor one whose blocks are not all stored — a hole the fill has not reached yet included: none of them holds (the first two are said once, as Trace segment cannot be built);
  • a segment whose block fails to trace — or its senders to recover, or its receipts to re-execute — keeps holding, pass after pass: pruned past, the segment would never be built nor backed up, and a block that fails every time is a fault to fix, not a reason to lose its segment. (Such a segment used to stop holding after 3 passes: a mainnet node lost senders segments 0–18 so, their Frontier transactions' high-s signatures refused by the Homestead rule — they are recovered now.) The failure is said once as a warning after 3 passes — Senders segment keeps failing to build: pruning keeps waiting for it until it builds, or free disk space falls below --prune-free-space, naming the segment, the block and the error — and named in the hold's reason until the block builds (Senders segment builds: the failure is cleared);
  • a trace segment keeps the block segment below it too while its first 256 blocks are not traced: they read those blocks' hashes with BLOCKHASH;
  • the receipts builder builds, and holds, only when the receipts segments are the block segments' size (--state-changeset-segment-size equal to --segment-size, as by default); otherwise it is off, said once at start;
  • below --prune-free-space (100 GB free by default) the holds are pruned past one segment a minute, lowest first — the block floor one segment past the lowest hold, the state history one changeset segment past the trace cache's — free space sampled again before each step, so a dip below it costs the segments that bring it back; what was not built of them is lost, each said as a warning. --prune-free-space 0 never releases them: a segment that never builds then keeps every block above it.

The node logs Block pruning waits for the trace cache: trace segment 47 is not built yet when a hold starts (and State history pruning waits for the trace cache), once per segment — followed by ; block 2350126 failed to build: … when a block of it fails; debug_status shows the block pruner's floor, the floor it wants and what holds it, the failing block included, as pruning. Segments built are then backed up before they are pruned in turn.

Where receipts live

  • --receipts none (the default) keeps none: a receipt request re-executes its block on its parent's state (100–300 ms on Sepolia's history, and the state history must reach it), and eth_getLogs re-executes the blocks of its range whose bloom matches, at most 1,000 a query.
  • --receipts store (on with --fast-full-archive) keeps them in the executor's stream — ≈ 85 GB on Sepolia, prunable on its own (--receipts-history) while the blocks stay: a receipt request renders from it (12–21 ms for a block of about 150 receipts, less with the senders kept), and eth_getLogs scans it. Without --execute it keeps the segment archive's receipts of the blocks the node keeps instead, beside them, each block's checked against its header on the way in.
  • Never in the blocks: a node's block segments are the archive's, whatever the mode.

The caches: speed for disk

Both are off by default; --fast-full-archive turns both on, and each has its own switch:

  • --trace-cache keeps the trace cache: a debug_trace* request is a lookup (0.4 ms) instead of a re-execution of its block (190–230 ms), which needs the state history at that block. --trace-cache-history N is the middle ground: recent traces instant, older ones on demand. Without it, the traces of a range fetched on request (debug_fetchRange) still come from the archive where it publishes them: the flag is for keeping the window's traces and tracing what the archive lacks, not for using the archive's.
  • --senders-cache keeps every transaction's sender (20 bytes): a block with its transactions, and its receipts, render without recovering each sender from its signature — a third of a block request's CPU on the fast archive, for 23 GB.

Both fill in the background, newest blocks first, so the blocks most asked for are ready first. Tracing runs at 15–20 blocks a second, so the whole chain's traces take about five days to build locally; the senders are recovered at about 500,000 a second on a 32-thread host, Sepolia's in under an hour. A node with an archive behind it takes whole sealed segments of both instead.

Compression

Flag Store Default Smaller choice Cost
--compression block segments s2 zstd locked at creation; slower reads and seals
--seal-compression-level block segments 6 (better) 10 (best) seal CPU, once per segment
--block-group-size block segments 64 larger locked; a point read decompresses a bigger group
--state-changeset-compression changesets, receipts stream s2 zstd slower historical reads; not locked
--state-db-zstd history index off on a cgo-less build pays for zstd in pure Go
--index-key-bytes hash index 8 4–6 more truncated-key collisions, each re-checked; locked
--index=false hash index on off no lookups by block or transaction hash

The trace cache needs no compression knob: its records are compressed once, when they are made, at DEFLATE's best level in chains of up to 64 KB of consecutive transactions (a block's traces repeat the same contracts and calls, which a shared compressor reuses) — about 8 times smaller than their JSON (Sepolia's traces: 9.2 TB of JSON, about 1.1 TB stored) — and served as they are. The senders are kept raw: 20-byte addresses barely compress.

Caches without the state behind them

The trace cache does not need the state: a block's cached traces serve without it, and so do the receipts the node keeps. So --state-history can be capped below their reach — or execution dropped altogether — while the ranges keep their traces and receipts from the archive. What goes is everything that reads state at those blocks: eth_call, the balance / nonce / code / storage getters, any trace the cache does not hold (another tracer configuration, a block the archive has no trace segment for) and the receipts the node does not keep, which are re-executed. That is a fine trade for inspecting history, and a poor one for a node that answers state queries: keep execution and the state history behind every block you need eth_call at.

The archive as cold storage

Whatever a window drops can come back: a node restarted with a larger window fetches the pruned block segments, changesets and receipts back from the archive it names (--archive-source), segment by segment, and the trace builder and the senders builder take whole segments of traces and senders from it rather than making them. A node that keeps a small window and an archive behind it holds little and loses nothing. Publishing: an upload node with --backup publishes every kind — blocks, changesets, the receipts stream, traces and senders — as each segment seals.

Ranges on request

Two debug methods turn the same knobs while the node runs:

  • debug_fetchRange(from, to, [kinds]) fetches a block range from the archive — the blocks, their receipts (kept beside the blocks, served without the state history), the traces (kinds) — in whole segments, so what arrives may reach past the blocks asked for. It is kept until it is dropped, across restarts: the block store records it with what it was asked for, and a restart takes it back — fetching again whatever of it is missing — while --block-history goes on pruning everything else.
  • debug_dropRange(from, to) drops the block segments lying wholly inside a range, with their cached traces and senders, at once, and forgets a range fetched there. Nothing brings them back while the node runs; a restart fills back what the configuration keeps.

A node pruned to a small window looks into old history without a restart: fetch the range, read it, drop it when done. The /monitor chart shows what the node holds as it changes, and a table of the fetches and drops under it: how far each is, how parallel it runs and for how long.

curl -s http://127.0.0.1:8550 \
  -d '{"jsonrpc":"2.0","id":1,"method":"debug_fetchRange","params":[6500000, 6599999]}'
# ... debug_status lists it under blocks.fetchedRanges: fetching (blocks, receipts), filling (traces), then "done" ...
curl -s http://127.0.0.1:8550 \
  -d '{"jsonrpc":"2.0","id":1,"method":"debug_dropRange","params":[6500000, 6599999]}'

The fetched range's top segment is taken on the archive's word, as a node that executes nothing takes its ranges — nothing between the window and the range is downloaded — and the segments below link to it. Its state comes only when asked for (kinds "state"): without it the range's blocks, stored receipts and cached traces are served, eth_call and re-executed receipts and traces at its heights are not (see Caches without the state behind them). The state is the one kind that is not per range: a read at a block resolves through every change above it, so it is all the state history from the range up to the head — on Sepolia 71 GB from block 6.5M — fetched from the archive and kept, like the blocks, until the range is dropped. A fetched range differs from one named in --extra-archive-blocks in how it comes: it is taken top-down on the archive's word, so nothing between the window and the range is downloaded; a configured range hangs from the stored history, which a node without peers walks the archive down to.

Running short of disk: --min-free-space

A node that fills its disk fails writes halfway — a segment half sealed, a state commit cut short. The disk guard keeps it from getting there: it samples the free space of every volume holding the node's data (--block-db, --state-db) every 10 s, and below --min-free-space (50 GB by default) every writer stops at its next step:

  • block import and the backfill, between chunks and segments;
  • archive downloads — block, history, receipts and cache segments — each of which also waits until its file fits above the floor, so a large one never takes the disk under it halfway;
  • execution, between blocks, with no block in flight;
  • the receipts fill and the receipts and trace caches, between segments and chunks; the history extension and the ranges fetched on request;
  • state snapshots, deferred to a later pass while there is no room for one (segment uploads go on: they only read).

Nothing else stops: RPC keeps serving what the node holds, and the pruners and debug_dropRange keep freeing space. Once every volume is back above the floor by a tenth of it (1 GB at the least: 55 GB for the default), the writers carry on by themselves — free some space, or restart with a lower floor. A snap sync's restore waits as well, until the snapshot and its unpacked state fit (2.5 × the snapshot above the floor).

The node logs Free disk space below --min-free-space when it pauses — again every 10 minutes while it stays paused, naming the writers held — and Free disk space is back when it resumes; debug_status shows the guard as diskSpace. --min-free-space 0 turns it off.

Before it comes to that, pruning stops waiting. Pruning keeps a block segment until the traces, senders and receipts built from it are built, and the state history until the trace cache has traced from it — but below --prune-free-space (100 GB by default) free on any of the same volumes, it goes past those holds one segment a minute, lowest first: the block pruner one segment past its lowest hold an evaluation, the state history one changeset segment past the trace cache's, free space sampled again before each step — a dip below the floor costs the segments that bring it back, not every hold at once. What was not built of them is lost, each said as a warning (block pruning no longer waits for the trace cache, state history pruning no longer waits for the trace cache for one changeset segment); once free space is back above the floor by a tenth, pruning waits again. debug_status shows it as pruning.forced.

The backup is not released by default: a sealed segment stays until --backup holds it whatever the free space — uploads go on while the writers pause, so the wait never stalls the node, and a node deployed to back the chain up must not lose segments unasked. Only a --prune-free-space given explicitly releases it too: below it a store drops a sealed segment the archive does not hold yet, one of a kind a minute and only once the archive's manifest has been read, each said as a warning (pruning a sealed segment the archive does not hold yet; it is never backed up). The startup line Pruning waits for the data built from the blocks and for the backup says what the floor releases (releases).

The floor must be above --min-free-space — the builders pause there, so a hold would never clear — and the node refuses to start otherwise; left at its default below a raised --min-free-space, it is set to twice that. --prune-free-space 0 never releases the waits.

Strategies

The sizes below are Sepolia at 11.9M blocks; mainnet scales with its history.

Compact archive (the default)

Everything kept once, nothing derived stored: every block and the whole state history, for eth_call at any block; requests for a block's receipts or traces re-execute it.

umnode --network sepolia

Disk: blocks 450 GB + index 20 GB + state 195 GB — about 665 GB. A block's receipts and its traces take 100–300 ms to serve, re-executed; eth_getLogs re-executes at most 1,000 blocks a query.

Fast full archive

Every request a lookup: the compact archive plus the receipts, the traces and the senders, taken whole from the archive where it publishes them, made here where it does not.

umnode --network sepolia --fast-full-archive

Disk: the compact archive's 665 GB + receipts ≈ 85 GB + traces ≈ 1.1 TB + senders ≈ 23 GB — about 1.9 TB. The trace cache is most of it, and the first thing to cut if that is too much (--trace-cache=false keeps the receipts and the senders).

Archive, caches for the recent window

Everything kept, every block's receipts and senders, and the recent blocks' traces — where most requests land — ready to serve, older ones re-executed on request:

umnode --network sepolia --fast-full-archive --trace-cache-history 1000000

Disk: about 870 GB — the 665 GB compact archive, receipts ≈ 85 GB, senders ≈ 23 GB, and about 100 GB of traces for the last million blocks.

Recent window, fast

The usual RPC node: the last N blocks of everything, all of it ready to serve; older history comes back from the archive if a window is raised.

umnode --network sepolia --fast-full-archive \
  --block-history 1000000 --state-history 1000000

Disk (measured, 1M blocks with a snapshot sync): blocks 33 GB, state 133 GB, plus for the window about 15 GB of receipts, 3 GB of senders and 100 GB of traces — about 285 GB.

Smallest footprint

No receipts or caches kept (the default: receipts and traces re-executed on request), zstd where it is not locked, a short hash-index key:

umnode --network sepolia \
  --block-history 100000 --state-history 100000 \
  --compression zstd --state-changeset-compression zstd --state-db-zstd \
  --index-key-bytes 4

Disk (measured, 100k blocks with a snapshot sync): blocks 3 GB, state 109 GB — almost all of it the flat state, which any executing node holds. --receipts store --receipts-history 100000 keeps the window's receipts, for eth_getLogs over wide ranges without re-executing, at about 1.5 GB more. The --compression and --index-key-bytes choices apply to a new store only.

Blocks and receipts only

No execution at all: blocks, transactions, receipts and logs, nothing that needs state (eth_call, the getters, traces) — the receipts the archive's, each block's checked against its header on the way in.

umnode --network sepolia --execute=false --receipts store --block-history 1000000

Disk: the block segments for the window and their receipts — about 32 and 15 GB for the last million Sepolia blocks; no state store. Without --receipts store, blocks and transactions only.

Ranges from the archive, nothing executed

The fast way through history. A node without execution needs no state snapshot, no state history and no re-execution: it takes whole sealed segments from the archive — the blocks, their receipts, the call traces and the senders — for the ranges it is asked for, verifies the blocks (and the receipts against them), and serves all of it at once, every request a lookup:

umnode --network sepolia --execute=false --fast-full-archive \
  --block-history 1 --extra-archive-blocks 6500000:7499999

(Without --fast-full-archive — or --receipts store and --trace-cache — the node keeps no receipts or traces: there is nothing to re-execute them with.)

Disk: the ranges' share of the full figures plus one segment at the head — here 20 segments holding 113 million transactions, Sepolia's busiest stretch: blocks 70 GB, receipts 7 GB, traces 119 GB, senders about 2 GB — about 200 GB (a recent segment of 6 million transactions: about 6.5 GB). To look elsewhere, restart with other ranges: the old ranges' segments and caches go, the new ones arrive — or, without a restart, fetch and drop ranges on request. The full archive with every cache is the fastest node and the biggest (about 1.9 TB); shifting through segments serves the blocks it holds just as fast, at a fraction of the disk.

Everything state-related is out (see Caches without the state behind them), and the archive is the trust root — not for the receipts, which are checked against the blocks — for the caches, as for a state snapshot, and for the blocks — each range's top segment is taken on the archive's word, so nothing between the head and the range is downloaded, with peers or without (Trusting the archive; on a host without peers add --no-p2p).

Archive publisher

An upload node: everything kept — receipts, traces and senders too, so other nodes need not execute, trace or recover them — every kind published as it seals.

umnode --network sepolia --fast-full-archive --backup r2://bucket/prefix

Archive-fed node without peers

A node on a host that must not join the peer network: blocks downloaded from the archive, the state from its snapshot — and, with --fast-full-archive, the receipts, traces and senders too.

umnode --network sepolia --no-p2p --fast-full-archive \
  --block-history 1000000 --state-history 1000000

See Without peers.