This guide explains how to query historical Polygon PoS state over JSON-RPC using archive nodes. It covers Polygon's unique architecture (bor + Heimdall), the difference between full and archive nodes, practical methods for reading balances, state, and logs at historical blocks, and a reproducible Node.js script to verify archive support. It also addresses common pitfalls like rate limits and incomplete archive data.
Direct Answer: How to Query Historical Polygon State
To query historical Polygon PoS state over JSON-RPC, you need an archive node endpoint that retains the full state history of the bor execution layer. With such an endpoint, you can use standard Ethereum JSON-RPC methods—eth_getBalance, eth_call, eth_getCode, eth_getStorageAt, eth_getLogs, and eth_getProof—passing a historical block number or tag as the final parameter. Polygon PoS is EVM-compatible, so the RPC surface is identical to Ethereum's, but the ~2-second block cadence means that even a single day of logs spans over 43,000 blocks, making archive availability and query paging critical.
If your endpoint does not run in archive mode, historical state queries will either fail with an error (e.g., "missing trie node") or silently return the latest state, which is misleading. This guide explains the Polygon-specific architecture, provides a runnable script to test archive support, and outlines best practices for querying historical data reliably.
- Polygon PoS uses two layers: bor (EVM execution, geth fork) and Heimdall (Tendermint-based checkpointing).
- Archive nodes store all historical state, enabling queries at any past block.
- Standard JSON-RPC methods work, but you must specify a block parameter.
- Always verify your endpoint actually returns historical data, not a fallback.
Polygon PoS Architecture: Why History Is Different
Polygon PoS is a dual-layer network. The execution layer, bor, is a fork of go-ethereum (geth) and produces blocks every ~2 seconds. The consensus/checkpoint layer, Heimdall, is a Tendermint-based chain that periodically checkpoints the state of bor to Ethereum mainnet. For JSON-RPC queries, you interact directly with bor, which exposes the standard Ethereum JSON-RPC API plus a few Polygon-specific methods (e.g., bor_getAuthor, bor_getSnapshot). The execution/archive layering and the relevant client modes are described in the official Polygon full-node documentation, and the building on Polygon guide covers endpoint usage.
Because bor is a geth fork, the semantics of historical queries are the same as on Ethereum: a full node keeps only the most recent state (typically the last 128 blocks) needed for validation, while an archive node retains every historical state trie. The official Polygon documentation for running a full node describes the --gcmode=archive flag for bor to enable archive mode. This is documented behavior, not a vendor-specific claim.
The ~2-second block time has a direct impact on range queries. For example, a 7-day eth_getLogs window covers roughly 302,400 blocks (7 * 24 * 3600 / 2). On Ethereum (12-second blocks), the same window is about 50,400 blocks. This means Polygon archive queries are more likely to hit provider rate limits or timeouts, and you must page through results in smaller chunks.
- Bor: EVM execution client, geth fork, ~2s blocks.
- Heimdall: Tendermint chain, checkpoints state to Ethereum.
- Archive mode:
--gcmode=archivein bor (per official docs). - Full node: only recent state; archive node: all historical state.
Full Node vs Archive Node on Polygon
The distinction between full and archive nodes is the same as on Ethereum, but the storage requirements are amplified by Polygon's high block rate. A full node prunes historical state, keeping only the latest state and a limited set of recent blocks. An archive node stores every historical state, enabling queries at any block number.
For developers, the practical difference is that a full node cannot answer eth_getBalance at an old block (it will return an error or, if the provider is misconfigured, the latest balance). Archive nodes are essential for applications like analytics, audits, and historical DeFi position tracking.
When choosing an RPC provider, check whether they offer archive endpoints. Many public RPCs are full nodes only. OnFinality's RPC pricing page lists archive access options, and the API service provides dedicated endpoints. For self-hosted setups, follow the official Polygon node deployment guides.
- Full node: pruned state, limited historical queries.
- Archive node: full state history, required for historical RPC.
- Provider archive availability varies; verify before relying on it.
- Self-hosted: run bor with
--gcmode=archive.
Querying Historical State: Methods and Examples
The core JSON-RPC methods for historical state queries all accept a block parameter. For example, eth_getBalance(address, blockNumber) returns the balance at that block. Similarly, eth_call accepts a transaction object and a block number, eth_getCode and eth_getStorageAt take a block number, and eth_getProof can generate a proof for a historical block if the node supports it.
Below is a practical example using curl to query a balance at a specific block. Replace <YOUR_RPC_URL> with your archive endpoint.
curl -X POST <YOUR_RPC_URL> \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x...","0x1F4"],"id":1}'
The block number is in hex (0x1F4 = 500). If the endpoint is archive, you'll get a balance; if not, you may get an error or a fallback value.
- eth_getBalance: balance at a historical block.
- eth_call: execute a call at a historical block.
- eth_getCode: contract code at a historical block.
- eth_getStorageAt: storage slot value at a historical block.
- eth_getLogs: logs within a block range (requires archive for old ranges).
Reproducible Verification Script (Node.js)
The following Node.js script uses ethers v6 to test whether an RPC endpoint provides true archive data. It performs three checks: (1) reads a historical block by number, (2) reads a balance at that block and compares it to the latest balance, and (3) pages eth_getLogs over a bounded window with backoff. The script prints results and a table for you to fill in.
Assumptions: Polygon PoS mainnet, block number 50,000,000 (choose a block you know exists), and an RPC URL provided via environment variable POLYGON_RPC_URL. The script uses a well-known address (e.g., the USDC contract) for balance checks.
const { ethers } = require("ethers");
const RPC_URL = process.env.POLYGON_RPC_URL;
if (!RPC_URL) {
console.error("Set POLYGON_RPC_URL environment variable.");
process.exit(1);
}
const provider = new ethers.JsonRpcProvider(RPC_URL);
const TARGET_BLOCK = 50000000; // Choose a block you know exists
const ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; // USDC on Polygon
async function main() {
// 1. Read historical block
const block = await provider.getBlock(TARGET_BLOCK);
console.log(`Block ${TARGET_BLOCK}: timestamp=${block.timestamp}, txs=${block.transactions.length}`);
// 2. Balance at historical block vs latest
const balanceHistorical = await provider.getBalance(ADDRESS, TARGET_BLOCK);
const balanceLatest = await provider.getBalance(ADDRESS, "latest");
console.log(`Balance at block ${TARGET_BLOCK}: ${ethers.formatEther(balanceHistorical)} ETH`);
console.log(`Balance at latest: ${ethers.formatEther(balanceLatest)} ETH`);
// 3. Page eth_getLogs over a small window (e.g., 1000 blocks)
const startBlock = TARGET_BLOCK;
const endBlock = TARGET_BLOCK + 1000;
const logs = [];
const pageSize = 100;
for (let from = startBlock; from <= endBlock; from += pageSize) {
const to = Math.min(from + pageSize - 1, endBlock);
try {
const pageLogs = await provider.getLogs({
fromBlock: from,
toBlock: to,
address: ADDRESS
});
logs.push(...pageLogs);
console.log(`Fetched logs from ${from} to ${to}: ${pageLogs.length} logs`);
} catch (e) {
console.error(`Error fetching logs from ${from} to ${to}: ${e.message}`);
// Implement backoff: wait 1 second before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
// Retry once
try {
const retryLogs = await provider.getLogs({
fromBlock: from,
toBlock: to,
address: ADDRESS
});
logs.push(...retryLogs);
} catch (retryErr) {
console.error(`Retry failed: ${retryErr.message}`);
}
}
}
console.log(`Total logs fetched: ${logs.length}`);
// Fill in the results table
console.log("\nResults Table:");
console.log("| Check | Result |");
console.log("|-------|--------|");
console.log(`| Historical block accessible | ${block ? "Yes" : "No"} |`);
console.log(`| Balance at historical block differs from latest | ${balanceHistorical.toString() !== balanceLatest.toString() ? "Yes" : "No (possible fallback)"} |`);
console.log(`| eth_getLogs paging successful | ${logs.length > 0 ? "Yes" : "No"} |`);
}
main().catch(console.error);
Expected output shape: The script prints the block details, the two balances, and a log count. If the endpoint is not archive, the balance at the historical block may equal the latest balance (if the provider falls back) or throw an error. The results table helps you record the outcome.
To run: POLYGON_RPC_URL=https://your-rpc-url node script.js
- Uses ethers v6; install with
npm install ethers. - Replace the address and block number with your own.
- The script includes a simple backoff for rate limits.
- Fill in the results table to document your endpoint's behavior.
Common Failures and Fixes
When querying historical Polygon state, you may encounter several issues. Here are the most common and how to fix them.
1. "missing trie node" or "header not found" errors: This indicates the node is not an archive node. Solution: use an archive endpoint or run your own with --gcmode=archive.
2. Rate limiting (HTTP 429): Polygon's high block rate means range queries can be heavy. Solution: page in smaller chunks, add delays, and use a provider with higher limits. See the Polygon RPC 429s and rate limits guide.
3. Timeouts: Large eth_getLogs ranges can time out. Solution: reduce the range to a few thousand blocks and use pagination.
4. Silent fallback to latest state: Some providers may return the latest balance when asked for an old block. Solution: always compare with a known historical value or use a provider that explicitly supports archive.
5. Block number not found: If you specify a block number that is not yet finalized or is beyond the node's retention, you'll get an error. Solution: use a block number that is older than the node's pruning window (for full nodes) or ensure the block exists.
- Archive errors: switch to archive endpoint.
- Rate limits: page and backoff.
- Timeouts: reduce range size.
- Fallback: verify with a known value.
- Block not found: check block number and node retention.
Tradeoffs and Limitations
Archive nodes on Polygon require significant disk space and computational resources. The exact storage size grows over time and varies by provider; there is no single official number. As of 2026, a Polygon archive node can require several terabytes, but this is not a documented constant—it depends on the client version and pruning settings.
Provider archive availability is not universal. Many public RPCs are full nodes only. When using a third-party provider, check their documentation for archive support and any rate limits. OnFinality's RPC pricing page details archive options, but specific capacity numbers are documented per provider and may change.
Another limitation is that eth_getProof at historical blocks may not be supported by all archive nodes, as it requires additional state trie data. Always test with your endpoint.
Finally, Polygon's Heimdall chain does not expose historical state via JSON-RPC; all state queries go through bor. This means you cannot query checkpoint data via standard RPC methods.
- Storage requirements are high and grow over time.
- Provider archive support varies; verify before relying.
- eth_getProof historical support is not guaranteed.
- Heimdall state is not accessible via JSON-RPC.
Next Steps and Further Reading
Now that you understand how to query historical Polygon state, you can integrate archive RPC calls into your applications. For more context on Polygon's network, see the Polygon network overview. If you're new to historical queries, read the EVM cookbook on accessing historical blockchain data and the archive node vs full node guide.
For performance considerations, review the Polygon RPC latency guide and the rate limits guide. If you're choosing an RPC provider, compare options in the RPC Assistant for Polygon node types.
OnFinality offers dedicated RPC services with archive support; see the API service and pricing for details. Always test your endpoint with the script above to ensure it meets your historical data needs.
- Explore Polygon network details: Polygon network.
- Learn general historical data patterns: Accessing historical blockchain data.
- Understand node types: Archive node vs full node.
- Optimize RPC performance: Polygon RPC latency and rate limits.
- Choose a node type: Polygon node types (RPC Assistant).