Polygon RPC 429 errors are caused by exceeding rate limits set by public or provider endpoints. This article explains the mechanisms, common triggers, and provides practical fixes including batching, WebSocket subscriptions, narrowing eth_getLogs ranges, and implementing exponential backoff with Retry-After handling.
Direct Answer: What Does a Polygon RPC 429 Error Mean?
A Polygon RPC 429 error (HTTP 429 Too Many Requests) means your client has exceeded the rate limit imposed by the RPC endpoint you are using. This is a standard HTTP status code indicating that the server is throttling requests to protect its infrastructure. On Polygon (PoS), this typically occurs when you send too many requests per second or consume too many compute units (e.g., expensive eth_getLogs calls) within a short window.
The fix is not to increase the limit (which you cannot control on public endpoints) but to reduce your request rate and optimize your query patterns. This article explains the underlying mechanisms, how to diagnose the exact cause, and provides runnable code examples to implement robust solutions.
How Polygon RPC Rate Limiting Works
Polygon PoS exposes an Ethereum-compatible JSON-RPC API. Public endpoints (like the ones listed on Polygon's official documentation) and commercial providers (like OnFinality) enforce rate limits to ensure fair usage and prevent denial-of-service. These limits are typically based on two factors: requests per second (RPS) and compute units (CU). Compute units are a measure of the computational cost of a request; for example, eth_getLogs with a wide block range is far more expensive than eth_getBalance for a single address.
When you exceed these limits, the server responds with HTTP 429 and often includes a Retry-After header indicating how many seconds to wait before retrying. Some providers may also return a JSON-RPC error with code -32005 (limit exceeded) instead of a pure HTTP 429, so it's important to handle both cases.
It's crucial to note that the exact rate limit values and algorithms are implementation-specific. Public endpoints may have stricter limits than commercial providers. For instance, OnFinality's Polygon network page offers dedicated endpoints with higher limits, but the specific numbers are not publicly documented. Always check your provider's documentation for the most accurate details.
- Requests per second (RPS) limits: simple count of HTTP requests.
- Compute unit (CU) limits: weighted cost based on method and parameters.
- Retry-After header: tells you how long to wait before retrying.
- JSON-RPC error -32005: sometimes used instead of HTTP 429.
Common Triggers for 429 Errors on Polygon
Several common patterns lead to 429 errors on Polygon RPC endpoints. Understanding these triggers helps you diagnose and prevent them.
Wide eth_getLogs ranges: Scanning a large block range (e.g., 100,000 blocks) in a single call is extremely compute-intensive. This is a frequent cause of 429 errors, especially when indexing events for a token or contract.
eth_getBalance polling loops: Many applications poll the balance of a set of addresses every few seconds. If you have hundreds of addresses, this can easily exceed RPS limits.
getProof and archive state queries: Methods like eth_getProof require access to historical state and are expensive. Using them in loops or with wide parameters can trigger rate limits.
Bursty indexing: When a new block is mined, some applications immediately fire off a burst of requests to fetch all transactions and logs. This burst can exceed the per-second limit even if the average rate is low.
Diagnosing 429 Errors: Distinguishing from Other Transport Errors
Before implementing fixes, you need to confirm that the error is indeed a rate limit and not a network issue or a server error. Here's how to distinguish:
HTTP 429: The response status code is 429. The body may contain a JSON-RPC error object with code -32005 or a plain text message. The Retry-After header is often present.
HTTP 5xx: If you see 500, 502, or 503, the server is having issues, not your client. Retrying with backoff may help, but the cause is different.
Network timeouts: If the request times out without a response, it could be a network issue or the server is overloaded. This is not a 429.
To see the exact response, use curl with -v to display headers. For example, run the following command to make a simple eth_blockNumber call and inspect the response headers.
curl -v -X POST https://polygon-rpc.com \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Look for HTTP/1.1 429 in the response headers.
# If you see 429, note the Retry-After header value.Runnable Example: Health Check and Backoff in Node.js
Below is a self-contained Node.js script that demonstrates two things: a simple health check (eth_blockNumber) and a robust request function with exponential backoff that respects the Retry-After header. This script uses the built-in fetch API (Node.js 18+).
The script defines a function rpcRequest that sends a JSON-RPC request to a given endpoint. If the response status is 429, it reads the Retry-After header (or uses a default delay) and waits before retrying, with exponential backoff (doubling the delay each time) up to a maximum number of retries.
// polygon-rpc-backoff.js
// Run with: node polygon-rpc-backoff.js
const endpoint = 'https://polygon-rpc.com'; // Replace with your preferred endpoint
async function rpcRequest(method, params, retries = 5) {
let delay = 1000; // start with 1 second
for (let attempt = 0; attempt < retries; attempt++) {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 })
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : delay;
console.log(`Rate limited. Waiting ${waitMs}ms before retry ${attempt + 1}`);
await new Promise(resolve => setTimeout(resolve, waitMs));
delay *= 2; // exponential backoff
} else {
const data = await response.json();
if (data.error) {
throw new Error(`RPC error: ${data.error.message}`);
}
return data.result;
}
}
throw new Error('Max retries exceeded');
}
async function main() {
try {
const blockNumber = await rpcRequest('eth_blockNumber', []);
console.log('Current block number:', parseInt(blockNumber, 16));
} catch (error) {
console.error('Failed:', error.message);
}
}
main();Expected Results and How to Verify
When you run the script, you should see the current block number printed. If you are not rate limited, it will print immediately. If you are rate limited, you will see log messages about waiting and retrying.
To verify that the backoff works, you can intentionally send many requests in a loop. For example, modify the script to call rpcRequest('eth_blockNumber', []) 100 times in a tight loop. You should observe that after a few requests, you start getting 429 responses and the script waits before continuing.
Note: The public endpoint https://polygon-rpc.com may have strict limits. If you are building a production application, consider using a dedicated endpoint from a provider like OnFinality's API service to get higher limits and better reliability.
Fixes: Batch JSON-RPC Requests
One of the most effective ways to reduce the number of HTTP requests is to use JSON-RPC batch requests. Instead of sending multiple individual requests, you can send an array of request objects in a single HTTP POST. This reduces the overhead and helps you stay within RPS limits.
For example, if you need to fetch balances for 100 addresses, you can batch all 100 eth_getBalance calls into one request. The server processes them and returns an array of results. This is especially useful for polling loops.
Here's a Node.js example using fetch to send a batch request:
// batch-example.js
const endpoint = 'https://polygon-rpc.com';
const requests = [];
for (let i = 0; i < 100; i++) {
requests.push({
jsonrpc: '2.0',
method: 'eth_getBalance',
params: [`0x${i.toString(16).padStart(40, '0')}`, 'latest'],
id: i
});
}
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requests)
});
const results = await response.json();
console.log(results.length); // should be 100Fixes: Use WebSocket Subscriptions Instead of Polling
For real-time data like new blocks or pending transactions, polling is inefficient and can trigger rate limits. Instead, use WebSocket subscriptions. Polygon RPC supports the standard Ethereum WebSocket methods like eth_subscribe and eth_unsubscribe.
With WebSocket, you maintain a persistent connection and receive push notifications when events occur. This drastically reduces the number of requests. For example, to listen for new block headers, you can subscribe to newHeads.
Here's a minimal example using the ws package (install with npm install ws):
// ws-subscribe.js
const WebSocket = require('ws');
const ws = new WebSocket('wss://polygon-rpc.com'); // or your provider's WS endpoint
ws.on('open', () => {
ws.send(JSON.stringify({
jsonrpc: '2.0',
method: 'eth_subscribe',
params: ['newHeads'],
id: 1
}));
});
ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.method === 'eth_subscription') {
const block = message.params.result;
console.log('New block:', parseInt(block.number, 16));
}
});
ws.on('error', (err) => console.error('WS error:', err));Fixes: Narrow eth_getLogs Ranges and Split by Block Window
If you must use eth_getLogs, avoid wide block ranges. Instead, split your query into smaller block windows (e.g., 10,000 blocks each) and process them sequentially or in parallel with a controlled concurrency. This reduces the compute cost per request and helps avoid hitting CU limits.
For example, if you need to scan from block 10,000,000 to 10,100,000, you can make 10 requests of 10,000 blocks each. You can also use the fromBlock and toBlock parameters to specify the range.
Additionally, use the address and topics filters to narrow down the logs you're interested in. This reduces the amount of data returned and the compute cost.
// Example: split eth_getLogs into windows
const startBlock = 10000000;
const endBlock = 10100000;
const windowSize = 10000;
for (let from = startBlock; from < endBlock; from += windowSize) {
const to = Math.min(from + windowSize - 1, endBlock);
const params = [{
fromBlock: '0x' + from.toString(16),
toBlock: '0x' + to.toString(16),
address: '0x...', // optional
topics: [] // optional
}];
// Send request with backoff
const logs = await rpcRequest('eth_getLogs', params);
// Process logs
}Fixes: Cache State and Use Local Indexers
For data that doesn't change frequently, such as token balances or contract state, cache the results locally and only refresh them at intervals. This reduces the number of RPC calls significantly.
For heavy indexing workloads, consider running your own indexer or using a service like The Graph. This offloads the query load from the RPC endpoint entirely.
If you need archive data, consider using a dedicated archive node provider. OnFinality offers dedicated Polygon nodes that can handle high query loads.
Fixes: Exponential Backoff and Retry-After
Even with optimizations, you may still encounter 429 errors. Implementing exponential backoff with respect to the Retry-After header is essential for resilience. The example script above demonstrates this.
Key points: always read the Retry-After header if present; if not, use a default delay (e.g., 1 second) and double it on each retry. Set a maximum number of retries to avoid infinite loops.
Also, consider jitter (adding random delay) to avoid thundering herd effects when many clients retry simultaneously.
Tradeoffs and Limitations
While these fixes help, they have tradeoffs. Batching increases payload size and may hit request size limits. WebSocket subscriptions require maintaining a persistent connection and handling reconnects. Narrowing eth_getLogs ranges increases the number of requests, which could still hit RPS limits if not managed carefully.
Public endpoints are free but have strict limits and no SLA. For production applications, consider using a commercial provider like OnFinality, which offers higher limits, dedicated endpoints, and support. Check the pricing page for details.
Remember that rate limit policies vary by provider. Always consult your provider's documentation for specific limits and best practices.
Decision Checklist for Handling 429 Errors
Use this checklist to systematically address 429 errors on Polygon RPC:
- Confirm the error is 429 (check headers and body).
- Identify the trigger: wide eth_getLogs, polling loops, bursty indexing, etc.
- Implement batching for multiple independent requests.
- Use WebSocket subscriptions for real-time data.
- Narrow eth_getLogs ranges and split into windows.
- Cache state and use local indexers where possible.
- Implement exponential backoff with Retry-After handling.
- If sustained load is high, consider a dedicated endpoint from a provider.
- Check if the endpoint is public or provider-specific.
- Monitor your request rate and compute unit usage.
- Use the Retry-After header to schedule retries.
- Consider using a dedicated Polygon endpoint for production.
Next Steps and Further Reading
Now that you understand Polygon RPC rate limits, you can apply these techniques to your application. For more in-depth guidance, explore the Polygon RPC guide on OnFinality's RPC Assistant. You can also read our generic RPC 429 troubleshooting article for broader insights.
If you need a reliable endpoint for production, consider OnFinality's API service or dedicated Polygon nodes. Our pricing page offers transparent plans. For more learning resources, visit the OnFinality Learn section.