You ran a query against a slightly older block, and instead of an answer you got this:
missing trie node 2bca...e91f (path ) state 0x2bca...e91f is not available
Or a nearby historical-query failure such as:
header not found
missing trie node specifically means the state needed to answer your query is unavailable. header not found can also come from a bad block reference, an unsynced endpoint, or provider retention behavior, so treat it as a related symptom rather than the exact same error. This post explains what the state trie is, why a full node prunes it, exactly when you will trip missing historical state, and how to fix it; at the end we look at why an archive node, ideally one you do not have to operate, is the only real cure.
What “missing trie node” actually means
Ethereum stores account balances, contract storage, nonces, and code in a big Merkle-Patricia structure called the state trie. Every block has its own state root, and that root points down through a tree of intermediate “trie nodes” to the actual values. To answer “what was USDC’s balance for this address at block N”, the client has to walk the trie rooted at block N’s state root.
A full node does not keep every one of those tries forever. As the chain advances it prunes the trie nodes that belong to old blocks, keeping only what it needs to follow the head and serve recent queries. When you ask for state at a block whose trie has been pruned, the client tries to walk the tree, reaches a branch that is no longer on disk, and reports exactly that: a trie node is missing.
So the error is not a bug. It is the node telling you, accurately, that the historical state you requested was deleted to save disk. If you see header not found, first verify the block number/hash and sync status; if the reference is valid, it may be another sign that the endpoint cannot serve the historical context you requested.
Full nodes prune, archive nodes keep everything
This is the question underneath the error, so it is worth answering directly.
What is the difference between a full node and an archive node? Both validate every block and hold the full chain history of blocks and transactions. The difference is historical state. A full node keeps only recent state and prunes the rest. An archive node never prunes; it retains the trie at every block from genesis, so it can reconstruct any account’s balance or any contract’s storage at any height.
| Full node | Archive node | |
|---|---|---|
| Validates all blocks | yes | yes |
| Block + transaction history | yes | yes |
| Historical state (old balances, storage) | no, pruned | yes, all of it |
| State you can query | client/config dependent; often only recent state | every block since genesis |
| Disk (ballpark, June 2026) | client/config dependent | multiple TB; Geth archive can be 10 TB+ |
Answers eth_call at an old block |
no | yes |
The recent-state window is a moving target and varies by client and config. Geth-style defaults have historically kept only a short recent window of state available, often discussed around 128 blocks, but do not design around that number without checking your client’s current pruning/history settings.
When you hit it
The error only appears when a call needs state at a specific past block. The usual triggers:
eth_callat a historical block tag, for example reading a contract’s value “as of” some old block.getBalance/getStorageAtat an old block number, common when backfilling balances or reconstructing a position over time.debug_traceTransactionon an older transaction, because re-executing a transaction needs the exact pre-state of its block.- Indexers and analytics that walk history block by block; they sail along inside the recent window during testing, then fail the moment they reach back past it.
Calls that only touch the latest block, like a plain getBalance(addr) with no block tag, never see this error. That is why it often surprises people: the same code that works against head explodes the instant you pass an old blockTag.
Reproducing it
Here is the smallest program that provokes it. It reads an account’s balance far in the past, which forces the node to walk a historical trie:
import { ethers } from 'ethers';
// Locally, point RPC_URL at any HTTP or WebSocket endpoint.
// On BLAZED.sh today, talk to the co-located node over its local WebSocket (ws://eth:8545).
const RPC_URL = process.env.RPC_URL || 'ws://eth:8545';
const provider = RPC_URL.startsWith('ws')
? new ethers.WebSocketProvider(RPC_URL)
: new ethers.JsonRpcProvider(RPC_URL);
const VITALIK = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';
async function main() {
const head = await provider.getBlockNumber();
const oldBlock = head - 1_000_000; // ~4 months back, well outside the pruned window
const balance = await provider.getBalance(VITALIK, oldBlock);
console.log(`Balance at block ${oldBlock}: ${ethers.formatEther(balance)} ETH`);
}
main().catch(console.error);
Against a pruned full node, the getBalance call throws with missing trie node. Against an archive node, it returns the balance. To handle this cleanly in real code, match the error the same way you would match a range error, then decide whether to fall back or surface it:
// True when the node rejected us for lacking historical state, not for a transient reason.
function isMissingStateError(err) {
const msg = ((err && (err.error?.message || err.message)) || '').toLowerCase();
return msg.includes('missing trie node') ||
msg.includes('header not found') ||
msg.includes('state is not available') ||
msg.includes('state not available');
}
A runnable version that catches the historical failure and then repeats the same call at a recent block to prove the difference lives in the companion project at sample-projects/missing-trie-node. Copy .env.example to .env, set RPC_URL, run npm install, and run node index.js.
How to fix it
There are two honest answers, depending on whether you actually need history.
1. Stay inside the recent window. If you only thought you needed an old block, query the latest one instead, or a block within the last ~128. No archive node required; the state is still there. This is the right fix for “I accidentally passed an old blockTag.”
2. Use an archive node. If you genuinely need historical state, you need a node that kept it. Important: you cannot re-sync a full node and expect history to appear; a full node simply does not store it. Archive is a sync mode you commit to up front. By client:
- Geth: run with
--gcmode=archive --syncmode=full. Correct but heavy; as of June 2026, a Geth archive can require 10 TB+ depending on configuration and chain growth. - Erigon / Reth: flat-database designs can make archive-style access cheaper on disk and faster to sync, but exact requirements vary by version, pruning flags, and dataset.
- Nethermind: supports archive configurations as well.
Always check current client documentation before buying disks or committing to a sync mode; archive storage numbers age quickly.
On a hosted provider, archive state plus the trace_ and debug_ namespaces that depend on it typically sit behind plan-specific access controls or higher tiers, so the queries that need historical state are also the ones most likely to hit pricing/limit constraints. If that is your wall, see our rundown of Infura alternatives for high-volume Ethereum RPC.
The other fix: an archive node you don’t operate
The reason missing trie node is annoying is that the two real options are both costly: babysit a multi-terabyte archive yourself, or pay a gateway’s premium tier and call it across the public internet anyway.
BLAZED.sh takes a third path. It runs your container or script on the same server as a fully synced node with archive access included, reachable today over the node’s local WebSocket rather than a remote endpoint. Historical state draws from the same monthly credit allowance as the rest of your usage, with archive access included rather than gated behind a premium tier on the heavy debug_/trace_ calls that archive enables:
// Co-located with the node; historical state queries never leave the machine.
const provider = new ethers.WebSocketProvider('ws://eth:8545');
Note: IPC is the maximum-performance local transport in general and a future BLAZED.sh direction. Today on BLAZED.sh, use the local WebSocket above; it still keeps historical state queries on the host instead of sending them through a remote gateway.
This is the same lesson as our piece on eth_getLogs block range limits: the pain comes from reaching across a network into a multi-tenant gateway that has pruned, capped, or gated what you need. Move the code next to a node that kept the data, and that missing-state wall is no longer the bottleneck. If you want the groundwork first, our beginner’s guide to Ethereum nodes covers the RPC layer all of this sits on.
Conclusion
missing trie node is not corruption and usually not a sync problem; it means you asked a pruned full node for state it deliberately deleted. Related errors such as header not found deserve a quick block-reference and sync-status check before you classify them as missing history. If you do not need history, query a recent block. If you do, you need an archive node, and re-syncing a full one will not conjure it. The catch is that archive is expensive to run and often premium-priced to rent. Running your code on a co-located archive node gives you historical-state access without the multi-terabyte babysitting or a premium archive pricing tier.