RPC node rate limits: how to handle provider throttling
It's 2 AM. The team pushed a small UI change on Friday and went to bed confident.

Why your dApp suddenly hits a wall
By Sunday night, dashboards light up: p99 latency is climbing, users are posting screenshots of failed swaps, and somewhere between your frontend and the node, a quota is being eaten alive. You check the logs and find a flood of HTTP 429, Too Many Requests, responses from the primary RPC provider. The chain is healthy. The contracts are fine. But the bill at the node provider is, somehow, also fine — because requests may be rejected before they become billable usage.
This is one of the most common production incidents in Web3. It is also one of the most preventable, once you understand what is actually being limited and where the requests are coming from. A provider rate limit is rarely solved by adding one more retry loop. Usually, the limit is exposing a problem farther upstream: duplicated polling, an overly aggressive log query, a test suite sharing one endpoint, or a quota model nobody included in the architecture.
A 429 is not automatically a provider failure. It is often your system reporting that its request budget has no shape.
The useful question is not simply how to make the error disappear. It is which budget has been exhausted, whether the traffic is necessary, and what should happen when the provider says “not now.”
What “rate limited” actually means at the RPC layer
The HTTP 429 status code was formalized in 2012 in RFC 6585. In the RPC world, it appears when a client crosses one of several thresholds enforced by a node provider. Those thresholds are not interchangeable, and confusing them is how teams end up applying the wrong fix.
Requests per second
RPS is the limit developers notice first. A provider may cap the number of requests accepted during a short interval, either per API key, project, IP address, endpoint, or account. The exact scope matters. A second API key might help if the limit is genuinely key-scoped, but it will not help if the provider is enforcing an account-wide or IP-based ceiling.
Free-tier examples in the draft configuration are useful as planning figures: Alchemy at 25 RPS, QuickNode at 15 RPS, and Chainstack at 25 RPS. Treat those numbers as constraints for the relevant plan and product, not as universal properties of the providers. Limits can differ by chain, endpoint type, subscription, and method.
RPS limits also tend to have a burst component. A provider may tolerate a brief spike while still rejecting a sustained stream. Conversely, an endpoint can appear healthy during a short manual test and fail when a polling loop runs continuously. This is why “it worked in development” tells you very little about production behavior.
Compute units and other usage budgets
Many providers do not charge every JSON-RPC method equally. A simple eth_blockNumber call and a large eth_getLogs query can consume very different amounts of provider-defined compute units. Some plans also impose a monthly allowance. Once that allowance is exhausted, slowing the client does not restore it.
The draft’s Alchemy example uses roughly 30 million compute units for a free monthly quota. The important operational point is not the exact figure; it is that a monthly budget is a different failure mode from an RPS ceiling. Backoff can help with a temporary burst. It cannot create a new monthly allocation.
This distinction should appear in monitoring. A single “RPC errors” graph is not enough. Track at least:
- HTTP status by provider and method;
- request volume and concurrency;
- latency percentiles;
- compute-unit or credit consumption where the provider exposes it;
- remaining monthly quota;
- retry attempts and exhausted retries;
- the proportion of calls served by a fallback;
- the age of cached or indexed data used by the application.
If the project only watches 429s, it may discover the problem after the budget has already been spent.
Per-method and endpoint limits
Providers may apply different treatment to eth_call, eth_getLogs, trace methods, archive reads, or websocket subscriptions. A dApp that looks modest in terms of total RPS can still overload one expensive method.
Log queries deserve special suspicion. Asking for a wide block range, many addresses, or a large set of topics can create heavy responses and long-running work even when the request count is low. A provider may respond with a method-specific error, a timeout, a 429, or a provider-specific resource error. These are different symptoms of the same architectural issue: the application is asking the node to do too much at once.
A 429 is not always a code problem. Sometimes it is an architecture problem wearing code-problem clothes.
Where the request spike actually comes from
Before changing any infrastructure, audit what the frontend is actually sending. A pattern that appears in nearly every client engagement is request multiplication: a wallet badge, a balance ticker, a price hook, and a transaction-history widget each create their own public client and independently poll eth_blockNumber or fire eth_call on mount.
Five components making four reads per second creates 20 RPS before the user has done anything. Add several open browser tabs, a reconnecting wallet, a development hot-reload loop, and a second chain selected in the wallet, and a “small” application can reach a free-tier ceiling surprisingly quickly.
This is not primarily a provider problem. It is a state-synchronization problem. The first fixes are unglamorous but highly effective:
1. Use one shared client per transport and chain. Pass a configured client through application context, a dependency-injection layer, or a shared store. Do not let every component create its own client on import.
2. Coalesce subscriptions. One polling loop should feed all interested components through a store. There is no reason for every balance widget to call eth_blockNumber on its own timer.
3. Debounce route changes. A wallet-screen mount should not trigger a thundering herd across every hook. Batch or schedule reads after the route has settled.
4. Pause work when the tab is hidden. Browser applications often continue polling while nobody is looking at them. Visibility-aware polling is a simple way to remove background traffic.
5. Separate urgent reads from decorative reads. A transaction status update may need prompt confirmation. A token price label or block number in a footer usually does not.
6. Deduplicate identical calls. If three parts of the interface request the same block, balance, or allowance during the same interval, share the in-flight promise and reuse the result.
The reduction can be substantial without changing provider configuration at all. More importantly, it makes the remaining traffic understandable. A provider cannot tell whether ten identical calls came from ten business operations or ten React effects. Your application can.
Audit the methods, not just the totals
A request counter grouped only by endpoint hides useful detail. Record the JSON-RPC method and, where practical, a normalized description of the request. For eth_call, record the contract function or an internal operation name rather than dumping sensitive arguments. For logs, record the block-range width and the number of addresses or topics.
This quickly reveals patterns such as:
- an indexer query being repeated on every render;
- a token balance read being requested for every list row;
- a log scan starting from deployment on every page load;
- a wallet connector retrying while the user is offline;
- a test or preview environment pointing at the production API key;
- a health check calling an expensive method more frequently than the application itself.
Once the request source is visible, the right solution is often smaller than expected. A five-second cache, a narrower log range, or one shared polling loop can remove more load than a provider migration.
Client-side retries that do not make things worse
If you have genuinely outgrown the current tier, you need a transport policy rather than optimistic polling. Modern Web3 libraries provide retry primitives, but they still need to be configured with the provider’s behavior in mind.
Viem transport configuration accepts retryCount and retryDelay in the transport options. The important detail is that retryCount means retries after the initial request. A setting of retryCount: 3 therefore allows up to four attempts in total: the original request plus three retries. It does not mean three total attempts.
That distinction matters when estimating traffic. If a client sends 100 requests and all of them fail, retryCount: 3 can produce as many as 400 attempts before the operation is abandoned. A retry policy can turn a small outage into a much larger provider-side spike if it is applied indiscriminately.
A fixed retryDelay: 1000 means waiting one second between retries. That may be a reasonable starting point for a low-volume internal tool, but a fixed delay is a poor production default when many workers fail together. If every client retries after the same interval, they synchronize into another burst.
Exponential backoff with jitter is safer. Conceptually, the delay grows with each attempt and includes a random component: base * 2^attempt + random(0, jitter). The randomness prevents a fleet of browser tabs, CI jobs, or backend workers from retrying at the same moment.
The retry policy should also distinguish errors:
- Retry a 429 when the provider indicates a temporary limit and the operation is safe to repeat.
- Respect
Retry-Afterwhen the response includes it. That header is more useful than guessing a delay. - Retry transient network failures with a bounded policy.
- Be cautious with timeouts. A timed-out request may have reached the node and completed there, even if the client never received the response.
- Do not retry invalid parameters. A malformed block range or unsupported method will not become valid on the next attempt.
- Treat writes differently from reads. Repeating a read is usually harmless. Repeating a transaction submission requires attention to the transaction hash, nonce handling, and whether the provider accepted the raw transaction before the connection failed.
Retries should be bounded by an overall deadline. A UI request that waits through several long backoffs is not resilient from the user’s perspective; it is simply slow. For background synchronization, it can be reasonable to abandon the current cycle and let the next scheduled poll try again. For a transaction-status check, the application may show a pending state and continue reconciliation separately.
One thing retries cannot fix is monthly quota exhaustion. If the project has burned through its provider credits, no client-side backoff will mint more. The honest options are to upgrade, move some traffic to a secondary provider, reduce the work the dApp performs, or accept that the operation must wait for the billing cycle. Hiding the error behind a longer retry loop only consumes more compute and delays the visible failure.
Aggregating providers and shaping outbound traffic
Multiple providers are useful, but “add a fallback” is not a complete design. The application needs to know which calls can move between providers, whether the providers expose compatible chain data, and how to avoid sending the same overloaded request to every endpoint.
In ethers.js, FallbackProvider can combine multiple JSON-RPC providers and select another when one fails or falls behind its configured requirements. It is most useful in a backend, worker, or controlled client environment where you can set priorities, weights, and quorum behavior deliberately. In a browser, exposing several provider keys may create a security and quota-management problem, so a server-side RPC gateway is often easier to operate.
A setup that tends to age well looks like this:
| Concern | Primary path | Fallback path |
|---|---|---|
| Free-tier RPS | Alchemy endpoint | QuickNode endpoint |
| Geographic redundancy | Regional endpoint close to users | Endpoint in a separate region |
| Archive reads | Provider plan with archive support | Chainstack or another archive-capable provider |
| Historical logs | Indexer or dedicated RPC route | Narrow-range RPC queries |
| Transaction submission | Provider with reliable broadcast | Separate provider used only for submission recovery |
Do not make the list unnecessarily long. Two or three independent endpoints are usually easier to reason about than a chain of six. A long fallback sequence can turn a quick 429 into a request that waits through several timeouts. It can also multiply provider costs if the same expensive operation is attempted everywhere.
Independence matters more than brand count. Two URLs that resolve to the same underlying service may share account-level throttling, infrastructure, or outage conditions. A second region is useful for network locality, but it is not automatically a quota fallback. Check whether limits are applied per endpoint, project, account, IP, or method.
Queueing and concurrency control
A request queue is one of the most direct RPC throttling solutions. Instead of allowing every caller to open a request immediately, place work in a bounded queue and control how many operations are in flight. The queue can prioritize user-visible actions over background refreshes and discard stale work that no longer matters.
This is particularly valuable for log scans and historical reads. A user navigating away from a screen should not leave a large query running merely because a promise was created. Likewise, five overlapping requests for the same range should be merged or cancelled rather than allowed to compete.
Concurrency limits should not be confused with a strict RPS limit. A slow request can occupy a slot for a long time, while a fast method can still create a burst. In practice, teams often need both:
- a maximum number of concurrent requests;
- a pacing rule for new requests;
- a maximum queue length;
- cancellation or expiry for stale work;
- separate budgets for interactive and background traffic.
Proxies such as eRPC can provide some of this coordination in front of several RPC providers, but the same principles apply if the routing layer is built in-house.
Batching, caching, and indexing
JSON-RPC batching combines several calls into one HTTP request. It can reduce HTTP overhead and the number of transport-level requests, which is useful on unstable mobile connections or when a provider enforces a strict request-count ceiling. It does not necessarily reduce provider compute units. If the provider prices each call inside a batch separately, the compute budget remains essentially the same.
Batching also has limits. A very large batch creates a large response, increases tail latency, and can be rejected by the provider or an intermediary. It is most useful for a bounded group of related reads, not as a way to hide unlimited work.
Caching is often the cheaper win. eth_chainId rarely needs to be requested repeatedly. The latest block number changes regularly but can usually be shared for a short interval. Gas-price data, token metadata, and non-urgent balances can tolerate different cache windows depending on the application.
A cache should be explicit about freshness. A five-second in-memory cache may be fine for a dashboard label and inappropriate for a liquidation monitor. For mutable state, key the cache by chain, address, block context, and relevant call parameters. Avoid accidentally serving data from one network to another when users switch chains.
Historical reads are where an indexer earns its place. The Graph, a self-hosted Ponder deployment, or another indexing system can serve application-specific history without asking the RPC node to rescan the same event range for every user. The indexer does not eliminate RPC operations; it moves repeated work into a pipeline that can be monitored, checkpointed, and queried more efficiently.
Hardhat and Foundry without one shared test bottleneck
Local development and CI often create a separate rate-limit problem. Fork tests can issue thousands of calls, and parallel jobs may all point at the same provider key. A test suite that is acceptable on a paid plan can become an unexpected source of web3 rpc error 429 responses when a pull request starts several workers at once.
The fix is not to place an array of URLs in a Hardhat network configuration. Standard Hardhat network entries use one URL per named network. If you want explicit endpoint selection, define named networks and choose the network for the task or job.
For example, a Hardhat configuration can expose separate entries such as mainnetPrimary, mainnetSecondary, and mainnetArchive, each with its own url value read from environment variables. A fork task can then run against the selected named network rather than assuming that networks.mainnet.url is a built-in failover list. In CI, separate jobs can receive different network names or different environment variables, which spreads load without pretending that Hardhat itself will rotate endpoints inside one network definition.
That separation also makes failures legible. If the primary endpoint is throttled, the job log can say which named network was selected. It becomes possible to reserve the archive endpoint for tests that actually need historical state instead of sending every fork to the most expensive route.
Foundry has a comparable mechanism, but the configuration key is different. Define aliases in the [rpc_endpoints] section of foundry.toml, for example mainnet_primary, mainnet_secondary, and mainnet_archive, with their URLs supplied through environment variables. A fork command can then select the relevant alias with the --fork-url option.
Inside Solidity-based tests and scripts, Foundry’s vm.rpcUrl cheatcode can resolve a configured RPC endpoint by alias. That is the supported mechanism to choose between named endpoints in test code; eth_rpcUrl is not a standard Foundry configuration key or cheatcode. If the test needs to switch endpoints deliberately, make that choice visible in the test setup rather than hiding provider rotation in an ad hoc helper.
Neither Hardhat named networks nor Foundry’s RPC aliases automatically solve quota exhaustion. They give the project a clean way to route work. You still need to decide how CI jobs share keys, how many workers may run at once, and which tests are allowed to use archive access.
A practical CI policy can include:
- separate credentials for local development, pull requests, nightly tests, and production tooling;
- a lower worker count for fork-heavy jobs;
- a secondary named endpoint reserved for failover rather than used by every test by default;
- cached or prebuilt fixtures for state that does not need to be fetched from a live chain;
- explicit timeouts so a throttled job fails promptly instead of multiplying retries;
- metrics that distinguish test traffic from user traffic.
The goal is not to make the provider invisible. It is to stop a test runner from behaving like an accidental load generator.
Picking a provider that will not bite back
Once the client is well behaved, provider choice matters again. The headline RPS figure is only one part of the contract. When evaluating an RPC node provider rate limit solution, look at the shape of the limits and the escape routes available when the application reaches them.
Documented limits and billing semantics
Look for documented RPS, burst, compute-unit, credit, archive, and log-query limits. Free tiers are useful for prototypes, but production planning should be based on the actual plan assigned to the project. The example figures — Alchemy at 25 RPS and roughly 30 million compute units, QuickNode at 15 RPS — should be treated as plan-specific operating constraints, not permanent promises.
Also verify what counts toward usage. Does a failed request consume credits? Does a batched request count as one HTTP request but several method calls? Are websocket messages billed differently? Are archive methods available at all? These details change how much protection caching or batching provides.
Response headers and error vocabulary
Check whether the provider returns Retry-After on 429 responses and whether the value is consistently usable. If it does not, the client has to estimate a delay. Record provider-specific error bodies as structured metadata, but do not make the entire application depend on one vendor’s wording.
A good adapter translates provider errors into an internal vocabulary: rate limited, quota exhausted, invalid request, unavailable, timeout, and unsupported method. That keeps business logic independent from the current RPC vendor and makes it possible to route only the errors that are safe to route.
Method coverage
One provider may be generous with ordinary reads and restrictive with eth_getLogs. Another may support archive state but have weaker websocket reliability. Compare the methods your dApp actually uses rather than choosing from an abstract list of supported chains.
For log-heavy applications, ask about maximum block ranges, response-size limits, topic filtering, and archive depth. For fork testing, ask whether historical state is available at the blocks the suite uses. For transaction-heavy systems, evaluate broadcast behavior separately from read performance.
Upgrade path and operational fit
The cost of the next tier matters, but so does the way the upgrade works. A plan that requires a manual quota increase is a different operational risk from one that scales automatically. Know whether an account can receive alerts before exhaustion, whether traffic can be capped to prevent runaway bills, and whether there is a clear route to a dedicated endpoint.
The secondary provider should be selected for an operational reason, not merely added to a spreadsheet. It may provide geographic diversity, a different pricing model, archive capability, or a separate failure domain. If it has the same account-wide throttling behavior as the primary, it may add complexity without adding resilience.
A rate-limit policy that survives production
The most reliable systems make rate limiting a first-class part of the application rather than an exception handler added after the first incident.
Start with an inventory of RPC operations. Classify each one as interactive, transactional, background, or historical. Give each class a freshness target and a failure behavior. An interactive balance can show its last known value while a refresh is delayed. A transaction submission needs a clear pending state and reconciliation. A historical chart can use an indexer or a stale cache. A background poll can simply skip one cycle.
Then enforce budgets at the edge of the application. A shared client, request deduplication, bounded concurrency, backoff, and cache policy should apply before calls reach the provider. The provider’s limit is the final guardrail, not your traffic-shaping layer.
The layers fit together:
1. Audit first. Find request multiplication and expensive methods before changing infrastructure.
2. Remove unnecessary work. Share clients, coalesce polling, cache stable reads, and move historical queries to an indexer where appropriate.
3. Retry selectively. Use bounded exponential backoff with jitter, respect Retry-After, and remember that viem’s retryCount counts retries after the initial request.
4. Add independent fallbacks. Route only the calls that can safely move, and keep the fallback path observable.
5. Shape outbound traffic. Use queues, concurrency limits, batching, and cancellation for stale work.
6. Separate environments. Named Hardhat networks and Foundry’s rpc_endpoints make CI routing explicit instead of relying on one overloaded URL.
7. Watch the budget. Alert on rising usage and declining headroom, not only after 429s appear.
8. Know when to stop retrying. A depleted monthly quota is a capacity decision, not a transient network error.
This is the difference between reacting to an RPC limit and designing around one. A well-behaved dApp can still receive a 429; providers throttle healthy systems too. The difference is that the application knows what to do next, does not amplify the incident, and preserves the user-visible operations that matter most.
We have spent the past few paragraphs debugging one of Web3’s most common scars together. If the constant stream of throttling and stack traces has you wanting to look at something quieter for a few minutes, the kind of slower, more hopeful reading this roundup tends to curate is a nice counterweight between incidents.
The practical conclusion is simple: treat RPC capacity as a budget with owners, priorities, and failure modes. Audit the calls, control the traffic, configure retries with their real semantics, and make provider choice part of the architecture. That is how you keep a rate limit from becoming a production outage.