Monad RPC endpoints enforce rate limits per IP and per API key, with public free tiers being the strictest. Heavy patterns like looped eth_getBalance or unbounded eth_getLogs can trigger 429s. This article explains how to identify the limiting scope, reduce request volume via subscriptions and batching, cache state reads, and implement exponential backoff with Retry-After. Includes a runnable Node.js example and a troubleshooting checklist.
Direct Answer: What Triggers a Monad RPC 429 and How to Fix It
Monad RPC endpoints enforce rate limits per IP address and per API key, with public free tiers being the strictest. When you exceed these limits, the server responds with HTTP 429 Too Many Requests or drops/stalls the request. To fix it, you need to identify the limiting scope (HTTP vs WebSocket, per-IP vs per-key), reduce request volume via batching and subscriptions, cache state reads, and implement exponential backoff honoring the Retry-After header. For sustained load, move to a dedicated or authenticated endpoint.
Monad's high throughput (up to 10,000 TPS in testnet) means that a single request can be processed quickly, but the rate limit is still based on the number of requests, not the computational cost. This makes it easy to hit caps with simple loops that would be fine on slower chains.
- Public endpoints: strict per-IP limits, often 10-100 req/s.
- Authenticated endpoints: higher limits, but vary by provider.
- WebSocket connections: often have separate limits from HTTP.
- 429 responses include a Retry-After header; honor it.
How Monad RPC Rate Limiting Works
Monad is an EVM-compatible L1 with parallel execution and MonadBFT consensus. Its JSON-RPC interface is standard Ethereum, but the underlying node software may implement rate limiting differently. According to the official Monad documentation (docs.monad.xyz), public RPC endpoints are rate-limited per IP, and the limits are not published. Providers like OnFinality, QuickNode, and Dwellir offer their own endpoints with varying limits. Monad's JSON-RPC surface and documented limits are described in the official Monad JSON-RPC documentation.
The rate limiting scope can be per IP, per API key, or per WebSocket connection. HTTP and WebSocket are often metered separately. For example, a public endpoint might allow 100 HTTP requests per second per IP, but only 20 WebSocket messages per second. When you exceed the limit, you get a 429 with a Retry-After header, or the connection is dropped.
Monad's parallel execution means that a single eth_call can be processed in milliseconds, but the rate limiter still counts it as one request. This is different from first-gen L1s where the bottleneck is block time; on Monad, the bottleneck is your request rate.
- Per-IP caps: apply to all requests from a single IP, regardless of API key.
- Per-key caps: apply to requests using a specific API key, often higher than per-IP.
- WebSocket: separate limits, often lower message rate.
- Public free tiers: strictest, designed for development, not production.
Identifying the Limiting Scope
When you get a 429, the first step is to determine whether the limit is per IP or per key. If you are using a public endpoint without an API key, it's per IP. If you are using an authenticated endpoint, it could be per key. Check the response headers: some providers include X-RateLimit-Limit and X-RateLimit-Remaining.
Also, check if the limit is on HTTP or WebSocket. If you are polling with eth_getLogs over HTTP, you might hit the HTTP limit. If you are using WebSocket subscriptions, you might hit the WebSocket message limit. Use separate endpoints for HTTP and WebSocket to avoid cross-contamination.
To test, send a burst of requests and observe the response. If you get 429 after a certain number, that's your limit. Record the Retry-After header to know how long to wait.
- Check response headers for rate limit info.
- Use separate endpoints for HTTP and WebSocket.
- Test with a controlled burst to find your limit.
- Document the limits for your provider.
Reducing Request Volume: Subscriptions, Batching, and Caching
The most effective way to avoid 429s is to reduce the number of requests. Instead of polling for new blocks or logs, use eth_subscribe over WebSocket. This pushes data to you, so you only get updates when something changes.
For state reads like eth_getBalance or eth_call, cache the results locally and refresh them periodically. For example, if you need to display balances, fetch them once and update every 10 seconds instead of every second.
JSON-RPC batch requests allow you to send multiple calls in a single HTTP request. This is especially useful for loops. Instead of sending 100 eth_getBalance requests, send one batch of 100. This reduces the number of HTTP requests, which is what the rate limiter counts.
For eth_getLogs, avoid unbounded ranges. Use block ranges that are small enough to return quickly, and paginate if necessary. Monad's high throughput means that a large range can return many logs, but the request itself is still one request.
- Use eth_subscribe for newHeads and logs instead of polling.
- Cache state reads and refresh periodically.
- Use JSON-RPC batch requests to combine multiple calls.
- Limit eth_getLogs ranges and paginate.
- Consider using a dedicated endpoint for high-volume production.
Implementing Exponential Backoff with Retry-After
When you do get a 429, you should retry with exponential backoff. The Retry-After header tells you how many seconds to wait. If it's not present, use a base delay and double it each retry, up to a maximum.
Here's a Node.js example using ethers.js that demonstrates a capped request loop with backoff and batching. It fetches balances for a list of addresses, batches them, and retries on 429 with exponential backoff.
The example uses a public Monad RPC endpoint (replace with your own). It first tries to batch all balance requests. If that fails with 429, it retries with backoff. The expected output is a list of balances.
- Always honor Retry-After if present.
- Use exponential backoff with jitter to avoid thundering herd.
- Set a maximum retry count to avoid infinite loops.
- Log retries for debugging.
const { ethers } = require('ethers');
const RPC_URL = 'https://rpc.monad.xyz'; // Replace with your endpoint
const provider = new ethers.JsonRpcProvider(RPC_URL);
const addresses = [
'0x...', // replace with actual addresses
'0x...',
'0x...',
];
async function getBalancesWithRetry(addresses, maxRetries = 5) {
let retries = 0;
let delay = 1000; // start with 1 second
while (retries <= maxRetries) {
try {
// Batch all balance requests into one JSON-RPC batch
const batch = addresses.map((addr) => ({
method: 'eth_getBalance',
params: [addr, 'latest'],
id: addresses.indexOf(addr),
jsonrpc: '2.0',
}));
const results = await provider.send('eth_batch', batch); // Note: eth_batch is not standard; use provider.send with batch? Actually ethers doesn't support batch directly. Use provider.send for each? Better to use a custom batch.
// For simplicity, we'll use Promise.all with individual calls, but that's not batching.
// To truly batch, use a custom HTTP request. Here's a simple batch using fetch:
const response = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batch),
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const wait = retryAfter ? parseInt(retryAfter) * 1000 : delay;
console.log(`429 received. Waiting ${wait} ms before retry.`);
await new Promise(resolve => setTimeout(resolve, wait));
delay *= 2; // exponential backoff
retries++;
continue;
}
const data = await response.json();
if (data.error) {
throw new Error(data.error.message);
}
// data is an array of results
const balances = data.map((item) => ethers.formatEther(item.result));
console.log('Balances:', balances);
return balances;
} catch (error) {
console.error('Error:', error.message);
if (retries >= maxRetries) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2;
retries++;
}
}
}
getBalancesWithRetry(addresses).catch(console.error);
// Expected output: an array of balances in ETH, e.g., ['0.123', '0.456', '0.789']Common Failures and Fixes
One common failure is using a loop to fetch data for many addresses without batching. This quickly hits the per-IP limit. Fix: batch requests or use a dedicated endpoint.
Another failure is polling for logs every second. This is inefficient and triggers 429s. Fix: use eth_subscribe for logs.
Unbounded eth_getLogs ranges can cause timeouts or large responses, but they still count as one request. However, they can be slow and may be dropped by the server. Fix: limit the range and paginate.
Not honoring Retry-After can lead to repeated 429s and potential IP bans. Fix: always read the header and wait.
Using the same endpoint for HTTP and WebSocket can cause cross-limit issues. Fix: use separate endpoints.
- Looping without batching: use batch requests.
- Polling for logs: use subscriptions.
- Unbounded getLogs: limit range and paginate.
- Ignoring Retry-After: implement backoff.
- Mixing HTTP and WebSocket: separate endpoints.
Tradeoffs and Limitations
Batching reduces the number of HTTP requests but increases the payload size. Some providers have limits on batch size (e.g., 100 items). If you exceed that, you may get an error.
Subscriptions over WebSocket are great for real-time updates but require maintaining a persistent connection. If the connection drops, you need to resubscribe.
Caching state reads can lead to stale data. You need to balance freshness with request volume.
Dedicated endpoints cost money but offer higher limits and reliability. For production, it's worth the investment.
Monad's high throughput means that even a single request can be processed quickly, but the rate limit is still based on request count, not computational cost. This is a key difference from first-gen L1s.
- Batch size limits vary by provider.
- WebSocket connections need reconnection logic.
- Caching introduces staleness.
- Dedicated endpoints are paid.
- Rate limits are request-count based, not cost-based.
Troubleshooting Checklist
Use this checklist to diagnose and fix Monad RPC 429 errors.
First, identify the endpoint type (public vs authenticated) and the limiting scope. Then, check your request patterns. Finally, implement the fixes described above.
- Check the response headers for rate limit info and Retry-After.
- Determine if the limit is per IP or per key.
- Check if you are using HTTP or WebSocket, and if they are separate.
- Review your code for loops that can be batched.
- Replace polling with subscriptions where possible.
- Cache state reads and refresh periodically.
- Implement exponential backoff with jitter.
- Consider moving to a dedicated endpoint for production.
- Monitor your request rate and adjust accordingly.
Next Steps and Further Reading
Now that you understand Monad RPC rate limits, you can optimize your application to avoid 429s. For more details, check out the Monad RPC endpoints and guide for endpoint setup and latency considerations. If you're looking for a reliable provider, see our Monad RPC endpoints (RPC Assistant) for a list of options.
For general RPC 429 handling, refer to our generic RPC 429 handling guide. If you're new to Monad, start with the Monad mainnet page. For pricing details, see RPC pricing. And if you need a managed solution, explore our API service.
Remember to always test your implementation against your provider's limits and adjust accordingly. Happy building on Monad!