This guide explains how to query historical Monad chain data over RPC, covering archive vs full node behavior, the JSON-RPC methods for historical reads, and a practical ethers script to page eth_getLogs. It also covers common failures and tradeoffs, and recommends managed archive providers for deep history.
Direct Answer: How to Query Monad Historical Data
To query historical Monad data over RPC, you need an endpoint that retains historical state and logs. A standard full node only keeps recent state (pruned), so for reads at old block numbers—like eth_getBalance at block 1,000,000 or eth_getLogs over a wide range—you must use an archive-capable source. Monad is EVM-compatible, so you use the same JSON-RPC methods as Ethereum, but with Monad's sub-second block time, the number of blocks grows quickly, making efficient paging essential.
This article is the Monad-specific companion to the general OnFinality Learn guide on accessing historical blockchain data. We'll cover the node types, the exact RPC methods, and a runnable script to fetch historical logs reliably.
Monad Full Nodes vs Archive Nodes: What's Retained
Monad documentation distinguishes between full nodes and archive nodes. A full node typically prunes historical state, keeping only recent state (e.g., the latest 128 blocks, but this is implementation-specific and not documented as a fixed number). An archive node retains all historical state, allowing queries at any past block. For logs, both node types can serve eth_getLogs, but the availability of old logs depends on the node's retention policy; some providers may prune logs on full nodes.
Monad's official documentation draws the same full-versus-archive distinction and notes that historical state is served from archive sources (see the Monad JSON-RPC overview and the Monad node/archive documentation referenced from it); it does not specify exact retention windows. As a rule, if you need state at a specific old block (for example a token balance at block N), you need an archive-capable source. For logs, you may be able to query recent history on a full node, but for deep backfills an archive source is safer.
Because Monad uses deferred execution and parallel execution, block production is fast—sub-second. This means thousands of blocks per hour. A log query over a 1-day window might span 100,000+ blocks, which can overwhelm a node if not paged. Always check the provider's documentation for retention and rate limits; these are provider-specific and not standardized.
- Full node: retains recent state only; suitable for current reads and recent logs.
- Archive node: retains full historical state; required for eth_call, eth_getBalance, eth_getCode at old blocks.
- Logs: may be available on full nodes for a limited window; archive nodes typically retain all logs.
- Monad's fast block time means historical ranges are large; plan paging accordingly.
JSON-RPC Methods for Historical Reads
Monad supports standard Ethereum JSON-RPC methods. For historical data, the key methods are:
eth_getBlockByNumber: Fetch a block by number, including full transactions if requested. Use block tag 'earliest' or a hex block number.
eth_getLogs: Filter logs by address and topics over a block range. This is the primary method for historical event data.
eth_call: Execute a call at a specific block to read contract state (e.g., balanceOf). Requires archive node for old blocks.
eth_getBalance, eth_getCode, eth_getStorageAt: Read account state at a given block tag.
eth_getProof: Get an account and storage proof at a specific block, useful for trustless verification.
- Block tags: 'latest', 'earliest', 'pending', or a hex block number.
- For eth_getLogs, fromBlock and toBlock are required; use hex or tags.
- eth_call accepts a block parameter; use a hex block number for historical state.
- eth_getProof is available on archive nodes and can be used for cross-checking.
Practical Example: Paging eth_getLogs with ethers.js
Below is a self-contained Node.js script that pages eth_getLogs across a range, handling timeouts and backoff. Replace the RPC_URL with your own endpoint (e.g., from Monad RPC endpoints). The script fetches logs in chunks of 10,000 blocks and prints the count and a sample log.
The script uses ethers v6 and includes a simple retry with exponential backoff on 429 or timeout errors. It also logs progress so you can monitor long backfills.
// Requires Node.js 18+ and ethers v6: npm install ethers
const { ethers } = require('ethers');
const RPC_URL = process.env.RPC_URL || 'https://rpc.monad.xyz'; // Replace with your endpoint
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Configuration
const CONTRACT_ADDRESS = '0x...'; // Optional: filter by contract address
const FROM_BLOCK = 1_000_000; // Starting block (hex or number)
const TO_BLOCK = 1_100_000; // Ending block
const CHUNK_SIZE = 10_000; // Blocks per request
const MAX_RETRIES = 5;
const BASE_DELAY = 1000; // ms
async function getLogsWithRetry(filter) {
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return await provider.getLogs(filter);
} catch (error) {
if (error.code === 'SERVER_ERROR' || error.code === 'TIMEOUT' || error.code === 429) {
const delay = BASE_DELAY * Math.pow(2, attempt);
console.log(`Retry ${attempt + 1} after ${delay}ms: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}
async function main() {
const allLogs = [];
let from = FROM_BLOCK;
while (from <= TO_BLOCK) {
const to = Math.min(from + CHUNK_SIZE - 1, TO_BLOCK);
console.log(`Fetching logs from ${from} to ${to}...`);
const filter = {
fromBlock: from,
toBlock: to,
address: CONTRACT_ADDRESS || undefined,
};
const logs = await getLogsWithRetry(filter);
allLogs.push(...logs);
console.log(`Found ${logs.length} logs in this chunk.`);
from = to + 1;
}
console.log(`Total logs fetched: ${allLogs.length}`);
if (allLogs.length > 0) {
console.log('Sample log:', JSON.stringify(allLogs[0], null, 2));
}
}
main().catch(console.error);Expected Output and Verification
When you run the script, you'll see progress lines and a final count. The sample log will show the standard Ethereum log structure: address, topics, data, blockNumber, transactionHash, etc. Use this to verify your query.
To verify correctness, you can cross-check a known event. For example, if you're querying a token transfer, you can compare the total count with a block explorer's event list for the same range. Note that block explorers may have different indexing, so minor discrepancies are possible.
Fill in the table below with your results to document the behavior of your endpoint.
| Chunk Range | Logs Count | Time Taken (s) | Errors/Retries |
|-------------|------------|----------------|----------------|
| 1,000,000-1,010,000 | ... | ... | ... |
| 1,010,001-1,020,000 | ... | ... | ... |
| ... | ... | ... | ... |
| Total | ... | ... | ... |Common Failures and Fixes
When querying historical data on Monad, you may encounter several issues. Here are the most common and how to resolve them.
Error: 'header not found' or 'missing trie node' — This indicates the node does not have the historical state. You need an archive node. If you're using a public endpoint, switch to a provider that offers archive data.
Error: 'query returned more than 10000 results' — Many providers cap eth_getLogs results. Reduce your chunk size or narrow the range. The script above uses 10,000 blocks, but you may need to lower it to 1,000 or even 100 for dense logs.
Timeout errors — Monad RPC endpoints have documented timeouts (see Monad RPC timeouts and retries). The script includes retry logic, but you may need to increase the timeout in your provider settings.
Rate limiting (HTTP 429) — Providers enforce per-IP rate limits. The script backs off, but for large backfills, consider using a dedicated endpoint or spreading requests over time. See Monad RPC rate limits and 429s.
Provider behaviour on large log ranges varies: many JSON-RPC gateways cap a single eth_getLogs response by result count rather than by block count, which is why paging in fixed block windows (as in the example above) is the safe pattern on high-throughput chains like Monad.
- Always use an archive endpoint for historical state reads.
- If you get 'result too large', reduce chunk size.
- Implement exponential backoff for 429 and timeouts.
- Check provider documentation for specific limits.
Tradeoffs and Limitations
Querying historical data has inherent tradeoffs. Archive nodes are more expensive to run and often have higher latency for deep queries. Full nodes are faster for current data but cannot serve old state.
For dApps that need token balances at arbitrary past blocks, you must use an archive node. For log backfills, you can often use a full node if the range is recent, but for full history, archive is necessary.
Monad's fast block time means that even a few days of logs can be millions of blocks. This makes full backfills expensive in terms of RPC calls and time. Consider using a data indexing service for very large backfills, but for moderate ranges, the script above works.
Provider-specific rate limits and retention policies vary. Always check the provider's documentation. For a comparison of providers, use the RPC Assistant or consult the Monad RPC provider list.
- Archive nodes: higher cost, slower for deep queries, but necessary for historical state.
- Full nodes: fast, but only recent state; logs may be pruned.
- Large log ranges: use paging and consider indexing services.
- Provider policies: always verify retention and rate limits.
Next Steps and Further Reading
For production use, consider a managed archive RPC provider to avoid the overhead of running an archive node yourself. To select and compare concrete Monad providers (including whether they expose archive-capable historical endpoints), use the Monad RPC endpoints RPC Assistant page and the Monad RPC provider list.
For more on Monad RPC performance and reliability, see our guides on Monad RPC latency and optimization, Monad RPC timeouts and retries, and Monad RPC rate limits and 429s. You can also explore the Monad mainnet network page for endpoint details.
If you need to select a provider, use the RPC Assistant for Monad to compare options. And revisit the general OnFinality Learn hub for more guides.