Monad RPC timeouts stem from transport issues, heavy method-level calls, or rate limits. This article explains how Monad's fast block times and parallel execution affect RPC load, provides diagnostic steps using curl and eth_blockNumber, and offers runnable Node.js examples for retry with exponential backoff and endpoint failover.
Direct Answer: Why Monad RPC Requests Time Out and How to Fix Them
Monad RPC requests time out for three primary reasons: a transport-level timeout (the connection or read fails), a method-level timeout (the node takes too long to execute a heavy call like debug_traceCall or a wide eth_getLogs), or a 429 rate limit that causes the request to be queued or dropped. The fix is to first identify which stage is failing, then set explicit per-call timeouts, implement idempotent retries with exponential backoff, narrow your query ranges, and failover between endpoints with a health check. This article walks through each step with runnable code.
Monad's unique architecture—sub-second block times and parallel execution—means that what counts as a 'heavy' RPC call differs from other chains. A call that is fine on Ethereum may time out on Monad because the node is processing many transactions in parallel, and archive data may not be available on all endpoints. Understanding these mechanics is key to reliable RPC usage.
- Transport timeouts: connect/read failures, often due to network issues or endpoint unavailability.
- Method-level timeouts: heavy calls like debug_traceCall, debug_traceBlock, or large eth_getLogs ranges.
- Rate limits (429): provider-specific limits on requests per second or per day.
- MonadBFT's fast block times increase the frequency of state changes, making stale reads more likely.
Monad's Architecture: Why Timeouts Behave Differently
Monad uses MonadBFT, a consensus mechanism that produces blocks in well under a second (documented as sub-second, with finality in about 1 second). This means the chain state changes rapidly, and an RPC node must keep up with a high rate of new blocks. For developers, this has two implications: first, polling for new blocks or logs is inefficient—you should use subscriptions; second, heavy calls that scan large ranges of blocks or traces can take longer because the node is also processing new blocks concurrently.
Parallel execution is another key feature: Monad executes transactions in parallel, which increases throughput but also means that debug_traceCall or debug_traceBlock can be more resource-intensive than on sequential chains. Additionally, not all RPC providers offer archive data; if you request historical state or logs beyond the node's pruning window, you may get an error or a timeout. Always check whether your endpoint is a full node or an archive node, and adjust your queries accordingly.
- MonadBFT block time: sub-second (documented by Monad).
- Parallel execution: increases node CPU/memory load for trace calls.
- Archive vs full node: full nodes may not serve historical state; archive nodes are needed for deep history.
- Stale reads: because blocks are fast, a read may be served from an outdated state if the node is lagging.
Diagnosing the Timeout Stage
Before fixing a timeout, you must know where it occurs. Use curl with the -w flag to measure timing stages: time_connect, time_starttransfer, and time_total. This tells you if the failure is at the TCP connection, the HTTP response, or the JSON-RPC method execution. The QuickNode Monad error code reference and the Monad JSON-RPC overview document the error and method surface referenced here.
Also check the node's sync status with eth_syncing. If it returns true or an object, the node is still syncing and may not serve latest blocks. Compare eth_blockNumber from your endpoint against an independent reference (e.g., a public explorer or another provider) to detect lag. A lagging node can cause reads to time out because it is trying to catch up.
- Use curl -w to measure connect, starttransfer, and total times.
- Check eth_syncing to see if the node is syncing.
- Compare eth_blockNumber with an independent reference to detect lag.
- If time_connect is high, it's a network/endpoint issue; if time_starttransfer is high, the node is slow to respond.
curl -w "connect: %{time_connect}s, starttransfer: %{time_starttransfer}s, total: %{time_total}s\n" -X POST https://your-monad-rpc-endpoint -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Expected output shape:
# connect: 0.02s, starttransfer: 0.10s, total: 0.10s
# {"jsonrpc":"2.0","result":"0x...","id":1}Setting Explicit Timeouts and Retry Patterns in ethers.js and viem
Most RPC libraries have default timeouts that may be too short for heavy calls. In ethers.js, you can set a timeout per call using the request property or by using a custom fetch. In viem, you can pass a timeout option to the client. Always set a timeout that is generous enough for the method you are calling, but not so long that your application hangs.
For retries, use exponential backoff with jitter to avoid thundering herd. Crucially, only retry idempotent requests—reads are safe, but state-changing writes (eth_sendRawTransaction) should not be blindly retried because you might duplicate a transaction. Instead, check the transaction receipt or use a nonce manager.
- ethers.js: use provider.send with a custom timeout or a custom fetch wrapper.
- viem: pass timeout to createPublicClient or createWalletClient.
- Exponential backoff: start with 1s, double up to a max, add random jitter.
- Idempotent retries: only retry reads; for writes, check the tx hash or use a nonce manager.
// Node.js example: retry with exponential backoff for a read call
const { ethers } = require('ethers');
const RPC_URL = 'https://your-monad-rpc-endpoint';
const provider = new ethers.JsonRpcProvider(RPC_URL);
async function callWithRetry(method, params, maxRetries = 3) {
let delay = 1000; // start with 1s
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await provider.send(method, params);
return result;
} catch (err) {
if (attempt === maxRetries) throw err;
console.log(`Attempt ${attempt + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
await new Promise(res => setTimeout(res, delay + Math.random() * 500));
delay *= 2;
}
}
}
// Example: get latest block number with retry
callWithRetry('eth_blockNumber', []).then(blockNumber => {
console.log('Latest block:', parseInt(blockNumber, 16));
}).catch(err => {
console.error('Failed after retries:', err);
});
// Expected output shape:
// Attempt 1 failed: ... Retrying in 1000ms...
// Latest block: 123456Health Check and Failover Between Endpoints
A robust setup uses multiple RPC endpoints and fails over when one is unhealthy. Implement a health check that periodically calls eth_blockNumber and measures response time. If an endpoint fails or is too slow, switch to the next one. This is especially important for production applications where downtime is not acceptable.
The health check should also verify that the endpoint is not lagging behind the network. Compare the returned block number with a reference (e.g., from a public explorer API) and consider the endpoint unhealthy if the lag exceeds a threshold (e.g., 10 blocks).
- Health check: call eth_blockNumber and measure response time.
- Lag detection: compare with an independent reference.
- Failover: maintain a list of endpoints and rotate on failure.
- Use a library like @ethersproject/providers FallbackProvider or custom logic.
// Health check and failover snippet
const endpoints = [
'https://rpc1.monad.xyz',
'https://rpc2.monad.xyz'
];
async function checkHealth(url) {
const start = Date.now();
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_blockNumber', params: [], id: 1 })
});
const data = await response.json();
const latency = Date.now() - start;
return { ok: response.ok && data.result, blockNumber: parseInt(data.result, 16), latency };
} catch (err) {
return { ok: false, error: err.message };
}
}
async function getHealthyEndpoint() {
for (const url of endpoints) {
const health = await checkHealth(url);
if (health.ok && health.latency < 2000) {
console.log(`Using ${url} (block ${health.blockNumber}, latency ${health.latency}ms)`);
return url;
}
}
throw new Error('All endpoints unhealthy');
}
getHealthyEndpoint().then(console.log).catch(console.error);
// Expected output shape:
// Using https://rpc1.monad.xyz (block 123456, latency 120ms)Common Failures and Fixes
Here are the most common Monad RPC timeout scenarios and how to fix them. Each fix is actionable and does not require deep protocol knowledge.
If you are using debug_traceCall or debug_traceBlock, these are inherently heavy. Limit the number of blocks traced, use a smaller range, or use a dedicated tracing endpoint if available. Some providers offer separate endpoints for trace calls with higher timeouts.
For eth_getLogs, avoid scanning huge block ranges. Instead, use eth_subscribe to listen for new logs in real time, or narrow the range to a few thousand blocks. If you need historical logs, consider an archive node or a data service.
If you are polling for new blocks, switch to eth_subscribe for newHeads. Polling every second can hit rate limits and cause timeouts. Subscriptions are more efficient and reduce load on the node.
- debug_traceCall timeout: reduce trace depth, limit block range, or use a dedicated tracing endpoint.
- eth_getLogs timeout: narrow block range, use eth_subscribe for live logs, or use an archive node for historical data.
- eth_getStorageAt timeout: avoid reading storage from very old blocks; use a recent block or an archive node.
- Polling newHeads: switch to eth_subscribe to avoid rate limits and timeouts.
- 429 rate limit: implement exponential backoff and respect Retry-After headers.
Tradeoffs and Limitations
While retries and failover improve reliability, they come with tradeoffs. Retries increase latency and can amplify load on the RPC provider, potentially triggering rate limits. Failover adds complexity and may cause inconsistent state if endpoints are not perfectly synchronized.
Also, not all methods are idempotent. For state-changing transactions, retrying can lead to duplicate transactions. Use a nonce manager or check the transaction receipt before retrying. Additionally, some providers have specific limits on trace calls or historical data; always check their documentation.
Monad's fast block times mean that a read may be served from a slightly stale state if the node is lagging. For applications that require strong consistency, consider using a provider that guarantees fresh data or implement a lag check before critical reads.
- Retries increase latency and provider load.
- Failover may cause inconsistent reads if endpoints are not in sync.
- State-changing writes require careful retry handling.
- Provider-specific limits: always check the provider's documentation for rate limits and method support.
Next Steps and Further Reading
Now that you understand Monad RPC timeouts, you can apply these patterns to your own applications. For a deeper dive into Monad's network specifics, see the Monad mainnet page. If you need a list of public endpoints, check the Monad RPC endpoints guide.
For more on rate limits and 429s, read our Monad RPC rate limits and 429s article. If you're experiencing timeouts on other chains, the generic RPC timeout diagnosis and fixes guide is a good resource. And if you're considering a commercial provider, review our RPC pricing and API service pages.
Finally, explore the OnFinality Learn hub for more tutorials and troubleshooting guides.
- Monad mainnet: Monad mainnet
- RPC endpoints: Monad RPC endpoints
- Rate limits: Monad RPC rate limits and 429s
- Generic timeout fixes: How to fix RPC timeout errors
- Pricing and API: RPC pricing and API service
- Learn hub: OnFinality Learn