For developers / JSON-RPC
History and retention
LiveThe node is an RPC index, not an archive. It keeps roughly two minutes of blocks, and a query past that answers null rather than erroring - which is why this page exists before you write an indexer rather than after.
What the node keeps
| Data | Retained |
|---|---|
| EVM block history | ~2 minuteskeep_evm_blocks() = 120,000 / EVM_BLOCK_MS - 480 blocks at the default. Older blocks return null and their logs become unqueryable |
| Mini-block history | 20,000KEEP_MINI_BLOCKS, roughly 200 seconds at the 10 ms scheduling target |
| Receipts and bodies | 200,000MAX_RECEIPTS, evicted first-in-first-out |
| Full history | not on the nodeit lives in Postgres behind the explorer, fed by the sequencer's spool. This RPC is an index, not an archive |
The block window is derived rather than fixed: keep_evm_blocks() = 120_000 / EVM_BLOCK_MS, which at the default cadence is 480 blocks. Logs are evicted with their block. Receipts and transaction bodies have their own first-in-first-out cap and can disappear before their block does.
Reads are served from a published copy-on-write snapshot and never take the execution lock, which is why a slow eth_call cannot stall block production and a stalled block cannot stall reads. That is the design that makes the window acceptable: the node optimises for serving the present quickly, and hands history to something built for it.
How this fails
Outside the window you get null, not an error
eth_getTransactionReceipt, eth_getTransactionByHash and every block lookup answer null for anything evicted. Nothing distinguishes "never existed" from "existed and has been dropped".
Three patterns that look correct and are not:
- Polling for a receipt with a long backoff. Wait long enough between attempts and the receipt can be evicted between two polls. It exists within about 10 ms of sending - poll early and often, not patiently.
- Backfilling from block 0.
earliestis the earliest retained block, and it moves forward as you work. A backfill loop chasing it never reaches a fixed start. - Reconciling a ledger nightly. By the time a nightly job runs, every block it wanted is gone.
What to do instead
The full history exists - it is streamed from the sequencer into Postgres behind the explorer, which is built to be an archive. So:
- For the present - the last few seconds - use the node. Subscriptions and
eth_getLogsover a narrow range are what it is fast at. - For the past, read the explorer. It has call traces too, which the node does not expose at all since there is no
debugnamespace. - To keep your own history, subscribe and persist as events arrive rather than querying for them later. The mini-block stream is the highest-fidelity source; a slow handler is dropped from it, so push to a queue and process elsewhere.
// Right: capture as it happens, persist immediately.
socket.send(JSON.stringify({
jsonrpc: "2.0", id: 1, method: "eth_subscribe",
params: ["logs", { address: MY_CONTRACT }],
}));
// Wrong on this chain: a range that has already scrolled out of the window
// returns an empty array rather than an error, so this looks like "no events".
await client.getLogs({ address: MY_CONTRACT, fromBlock: 0n, toBlock: "latest" });And if you key an event store on (blockNumber, logIndex), include the transaction hash: logIndex here is enumerated per transaction, so that pair is not unique.
Receipts across a restart
State survives a restart; the receipt index does not
The receipt and log index is held in memory. An acknowledged transfer's receipt has been observed disappearing after a sequencer restart while the balance change survived correctly - so a receipt is not a durable record of anything. Treat the explorer as the record of what happened, and the node's receipt as a fast, perishable acknowledgement.
State resets
Phase 0 chain state may be reset without notice, which deletes balances, contracts and history. If you deploy something you care about, keep the deployment script rather than the address - and expect to redeploy.
On a stack you run yourself, docker compose down -v is the deliberate version of the same thing: it wipes the sequencer state, the data-availability objects, the local L1 and the explorer's Postgres together.