blockchainsv
Developer Tools & Infrastructure·August 03, 2026·15 min read

RPC node fallback setups: avoiding production rate limits

An HTTP 429 is not a transient inconvenience. It is an upstream refusal. Your application exceeded a provider's admission threshold, and the provider has stopped accepting a class of requests for a defined interval.

RPC node fallback setups: avoiding production rate limits

For a dApp, the failure propagates fast. eth_call requests fail. Pending transaction views become stale. Indexers miss polling windows. A frontend retries into the same exhausted quota. Then the retry loop becomes the attack vector against its own infrastructure.

A production-grade RPC node provider fallback configuration does not mean placing three URLs in an environment file. It means defining which requests can be retried, which responses require agreement, which provider failures trigger demotion, and which state mutations must never be replayed blindly.

The distinction is operational. It is also a security boundary.

A fallback is not redundancy until failure detection, retry policy, and response validation are deterministic.

HTTP 429 is a capacity signal, not an exception to swallow

Most RPC providers return HTTP 429, Too Many Requests, when a client crosses an RPS, daily-request, compute-unit, or concurrency limit. The response may include a Retry-After header. It may express a delay in seconds or an HTTP date. It may be absent.

That inconsistency matters. A client that assumes a universal header format has built a parser bug into its incident path.

Free-tier thresholds are low enough to be crossed by ordinary frontend behavior. Alchemy's free tier enforces 25 requests per second or 30 million Compute Units per month. QuickNode's free tier has a 15 RPS limit. Infura's free plan has been associated with a 100,000-request daily ceiling. Exact accounting differs by provider and method. eth_getLogs is not equivalent to eth_chainId. Archive reads are not equivalent to block-head polling. Batch requests do not make cost disappear.

The most common cause is not one user. It is request multiplication:

1. A frontend mounts several components, each creating its own public client and block watcher.

2. Every watcher polls eth_blockNumber.

3. A new block triggers repeated eth_call reads for the same contracts.

4. A transient provider error activates unbounded retries.

5. Multiple browser tabs repeat the sequence.

6. A backend indexer and the frontend consume the same credential pool.

The apparent failure is a 429. The failed invariant is simpler: request volume was not bounded at the application boundary.

Do not handle 429 with an immediate retry. That turns a quota breach into a sustained burst. Respect Retry-After when present. Otherwise use bounded exponential backoff with jitter and a finite retry budget. More importantly, route the next attempt to a provider that is not sharing the same quota domain.

Two endpoints under the same account are not independent infrastructure. They are two doors into the same exhausted meter.

Separate reads, writes, and subscriptions before adding fallbacks

A multi RPC provider setup for Web3 becomes unsafe when it treats every JSON-RPC method as interchangeable. They are not.

Read methods can often be retried. Broadcast methods cannot be handled with the same logic. Subscription traffic has another failure model entirely.

Request classTypical methodsSafe fallback behaviorPrimary failure mode
Stateless readseth_call, eth_getBalance, eth_getCodeRetry on a separate upstream; compare where correctness is materialInconsistent block context
Historical querieseth_getLogs, block and receipt queriesPartition ranges; retry bounded chunks; require archive capability where neededTimeouts, range limits, pruned history
Block-head readseth_blockNumber, latest block queriesPoll through ranked endpoints; reject lagging headsStale upstream response
Transaction broadcasteth_sendRawTransactionPreserve raw signed payload; query for known hash before rebroadcastDuplicate or ambiguous submission
WebSocket subscriptionseth_subscribeReconnect, resubscribe, backfill missed blocks by numberMissed events during disconnect

The transaction path deserves special treatment. A signed transaction has a deterministic hash. After a timeout or transport failure, the client does not know whether the upstream accepted and propagated it. Rebroadcasting the same signed payload is generally preferable to signing a replacement transaction, but the application should first query known providers for the transaction hash or receipt.

Never let generic retry middleware sign again. Signing is a state mutation in the application domain. It must be explicit.

For event systems, WebSocket failover is not enough. A reconnect establishes a new subscription from the present. It does not restore notifications missed while the socket was absent. Track the last finalized or confirmed block processed, reconnect, then backfill logs over the gap. Deduplicate by transaction hash and log index. Without that cursor, "high availability" is only a persistent stream of incomplete data.

There is a useful parallel with short-horizon market data systems: a late quote is not merely slow data when a decision depends on sequence. The same is true of block-head and event consumption. Freshness is part of correctness.

Viem fallback transports: ordered failure handling with explicit limits

Viem provides the cleanest client-side path for most applications that need an RPC fallback implementation without operating middleware. Its fallback transport accepts multiple transports and advances to another candidate when a request fails.

The default behavior is conservative but not infinite. Viem uses a default retryCount of 3 and a base retryDelay of 150 ms, with exponential backoff. These defaults are reasonable for idempotent reads. They should not be copied mechanically into every workload.

A price-sensitive UI may prefer a short deadline and a fast alternate endpoint. An indexer performing historical log retrieval may need fewer concurrent requests, longer transport timeouts, and smaller block ranges. A backend issuing authorization-sensitive reads should pin a block number or block tag deliberately rather than accepting whichever "latest" response arrives first.

The transport order is policy. Put providers in that order only after measuring them against the chain, region, methods, and account plan that the application actually uses.

A durable Viem design has four layers:

1. A transport pool with independent providers. Mix accounts and vendors where the availability requirement justifies it. A primary and backup credential from one vendor protect against a bad endpoint. They do not protect against a vendor-wide throttle policy or regional outage.

2. Method-aware timeout budgets. A plain eth_call should not inherit the timeout used for eth_getLogs across a large range. The former needs fast failure. The latter needs controlled work partitioning.

3. Finite retry behavior. Retry only transport-level errors, selected server failures, and 429 responses after a bounded delay. Do not retry deterministic JSON-RPC errors such as an invalid parameter or a reverted simulation as though another provider can repair the request.

4. Block-context control. If several dependent calls must observe the same chain state, obtain a block number first and pass it through the read sequence where the client and contract interface permit it. Otherwise, a fallback can return internally inconsistent views across a block boundary.

Viem also supports transport ranking. It pings configured endpoints every 10 seconds and ranks them using a weighted moving score. The default weights favor stability at 0.7 and latency at 0.3. That ordering is correct. A fast endpoint that intermittently fails is not fast in aggregate.

But ranking is not consensus. It chooses a preferred upstream. It does not prove that an answer is correct.

Endpoint latency is a performance metric. Endpoint agreement is an integrity metric. Do not merge them.

Transport ranking is effective for broad read traffic: balances, contract metadata, current head, and UI queries. It is insufficient on its own for data that triggers an irreversible action. If a response determines liquidation eligibility, withdrawal state, or settlement parameters, the read path needs stronger conditions than "the lowest-latency endpoint answered."

Ethers.js FallbackProvider is a quorum mechanism, not a simple failover switch

Ethers.js takes a different approach. FallbackProvider can dispatch requests to multiple backends concurrently. Providers are assigned priority, weight, and stallTimeout. Lower priority values are favored. When a provider does not respond within its stall timeout, the next provider can be engaged.

The critical property is quorum.

By default, FallbackProvider requires agreement from 50% of the total provider weight, rounded up. This is materially different from sequential failover. A single endpoint's answer is not automatically accepted merely because it answered first.

That model is valuable where divergent upstream state is a real operational risk:

  • a provider is behind the chain head;
  • a load balancer routes one request to a degraded node;
  • an endpoint has an incorrect cache layer;
  • a chain reorganization is being observed at slightly different moments;
  • a provider returns a malformed or incomplete response under load.

Quorum has cost. Multiple backends receive the request. That increases request count, provider billing exposure, and sensitivity to method-level quotas. It may also increase tail latency when agreement is delayed.

Use it selectively.

For example, an ordinary token balance display can tolerate a prioritized endpoint and a bounded fallback path. A backend calculating a withdrawal limit should use a fixed block number and a quorum configuration appropriate to the value at risk. If the providers disagree, fail closed. Return an explicit unavailable state. Do not select the most convenient answer.

Provider weights should represent trust and capacity, not marketing tiers. A provider with broad archive support and strong historical reliability may deserve greater weight for indexing. A low-latency endpoint in the same region may deserve priority for volatile UI reads but not extra authority in a quorum.

There is another constraint: agreement requires comparable requests. If one backend receives latest before a new block and another receives it after, disagreement is expected. Pinning a block tag changes the question from "what do you currently see?" to "what was the state at block N?" That is the deterministic query form.

A production ethers.js RPC fallback implementation should therefore document, per route:

  • whether the query is allowed to hit several providers concurrently;
  • the required block tag or confirmation depth;
  • the quorum weight;
  • the maximum wall-clock deadline;
  • the error class that permits a retry;
  • the behavior on disagreement.

This is more documentation than most teams write. It is also less expensive than diagnosing contradictory data after funds move.

eRPC moves policy out of every client

Client-side transports are enough for a single backend service or a narrow frontend. They become difficult to govern when multiple services, SDKs, workers, and deployments each carry their own provider list and retry rules.

At that point, put the policy behind a proxy.

eRPC is an open-source, fault-tolerant EVM RPC proxy designed for this role. Instead of every application process choosing an upstream independently, clients call one internal endpoint. The proxy owns upstream selection, routing, retries, cache policy, rate limiting, and observability.

eRPC scores upstream providers every 15 seconds. Its scoring considers error rates, latency quantiles, throttle rates, and block-head lag. A provider that begins returning 429 responses or trails the canonical head can be demoted without changing every client deployment.

This solves a coordination problem. It does not remove the need for design.

A proxy should preserve request identity and emit structured telemetry for every attempt. At minimum, record:

  • RPC method and chain ID;
  • selected upstream and fallback sequence;
  • HTTP status and JSON-RPC error code;
  • upstream latency and total request latency;
  • retry count and backoff duration;
  • observed block number or head lag;
  • cache result, if a cache exists;
  • tenant, API key, service, or route that created the traffic.

Without method-level telemetry, a team sees "provider error rate increased." That statement has no diagnostic value. The useful statement reads more like this: a worker pool's log query exceeded the range accepted by upstream A, retried, and then began consuming a disproportionate share of upstream B's quota during the backfill window — leaving no headroom for unrelated traffic until the backfill completed. Whether the exact share is large or small matters less than the structure of the observation: one workload's failure cascading into a sibling provider's budget is a class of incident that aggregate counters cannot see.

The proxy is also the right layer for shared rate budgets. A frontend, indexer, webhook worker, and API server should not compete blindly for one provider key. Assign quotas by service and method family. Reserve capacity for transaction broadcasting and correctness-critical reads. Shed low-value polling first.

Do not cache arbitrary latest reads without a block-aware invalidation rule. A cached eth_call is valid only relative to chain state, block tag, and sometimes the sender address. Cache immutable historical responses aggressively. Treat head-dependent data as short-lived and tied to a known block number.

Provider selection is a failure-domain exercise

"Use three providers" is not an architecture. The providers may share cloud regions, upstream node software, account ownership, or the same application-side bottleneck.

Select upstreams by the workload they must serve.

For a frontend, the key concerns are geographic latency, public-read capacity, WebSocket reliability, and graceful degradation. A wallet connection should not fail because an NFT metadata widget exhausted the only RPC key.

For an indexer, archive depth, eth_getLogs behavior, block-range limits, reorganization handling, and sustained throughput matter more than a low median latency number.

For a transaction relayer, broadcast propagation, mempool visibility, replacement behavior, and independent confirmation reads matter. The best endpoint for reads is not necessarily the best endpoint for sending raw transactions.

Cost also changes the architecture. A provider with generous RPS but compute-unit metering can punish expensive methods. Another may cap requests but price predictable throughput. The correct comparison is not a monthly headline price. It is cost per useful workload under peak, retry, and incident conditions.

Measure the following before setting a primary order:

MeasurementWhy it matters
Success rate by RPC methodA provider can be healthy for eth_call and weak for log queries
p50, p95, and p99 latencyTail latency triggers fallback and increases duplicate load
429 frequency and recovery timeThrottle behavior determines whether retries stabilize or amplify traffic
Block-head lagA successful answer from a stale node can still violate application assumptions
Historical range acceptanceIndexers fail on range limits long before ordinary reads fail
WebSocket disconnect and resubscribe behaviorEvent loss is an indexing defect, not a cosmetic outage
Account-level quota couplingSeparate URLs are useless if they drain one shared budget

Web3.js should not be the foundation for new production fallback work. It was officially archived on March 4, 2025. Viem and Ethers.js provide maintained abstractions with clearer transport and quorum behavior. A legacy application can isolate its existing Web3.js calls behind a proxy, but extending that dependency creates more migration surface.

The production implementation

A working production setup is rarely just code. It is a small set of decisions recorded somewhere a new on-call engineer can find them.

The minimum reasonable configuration has three named tiers. A read tier for stateless and historical queries, served by ranked providers with method-appropriate budgets and a block-tag discipline for any read that drives a state-sensitive action. A broadcast tier for eth_sendRawTransaction, dedicated keys reserved for relayer and submission paths, with explicit hash-query checks before any rebroadcast. A subscription tier for WebSocket consumers, with a cursor per consumer (last finalized block processed), reconnect and resubscribe logic, and a backfill path for missed ranges. Each tier has its own provider pool, its own quota budget, and its own observability.

A few practices separate a stable system from one that merely survives its first incident:

  • Test 429 paths in staging, not in production. A synthetic client that exceeds the daily quota, recovers on Retry-After, and resumes traffic will reveal whether the fallback correctly rotates to a different quota domain. Without that rehearsal, the first real 429 is the integration test.
  • Keep a primary and a secondary that are truly independent. Two endpoints from the same vendor, in the same region, under the same account, fail together. The fallback looks correct in code while providing zero additional availability.
  • Bound the recovery, not just the failure. When upstream recovers, traffic often returns in a synchronized burst that retriggers the throttle. A small jitter on recovery and a gradual ramp protect both the upstream and the application.
  • Treat transaction broadcast as a distinct subsystem. It should not share retry policy with reads. It should not be demoted by the same ranking signal that demotes a stale historical endpoint.
  • Document per-route behavior. The ethers.js quorum section already described what each route should declare. Keep that document current. A routing table written once and never updated is a routing table that lies.
  • Audit the cache rules. A cached eth_call returning yesterday's latest is a logic bug presented as a performance optimization. Block-aware invalidation is not optional.
  • Watch the block head, not just the response code. A provider can answer with a 200 status and a block number that trails the chain. From the application's perspective, that is a correctness failure dressed as success.

The longer version of this material would include chaos drills, regional failover playbooks, and contract-level patterns for cross-checking critical reads. Those deserve their own treatment. For most teams shipping today, the architecture above is enough to keep a 429 from becoming a user-visible outage — and that is the actual bar for a production RPC node provider fallback configuration.

FAQ

Why should I avoid retrying immediately after receiving an HTTP 429 error?
Immediate retries turn a quota breach into a sustained burst of traffic, which can exacerbate the issue and act as an attack vector against your own infrastructure.
How can I ensure my RPC fallback setup is actually redundant?
You must ensure that your providers are truly independent by using different vendors, accounts, or regions, as endpoints under the same account share the same exhausted meter.
What is the difference between sequential failover and quorum-based providers?
Sequential failover attempts endpoints one by one until a response is received, while quorum-based systems require agreement from multiple providers to ensure data integrity and prevent reliance on a single potentially stale or incorrect node.
How should I handle transaction broadcasting in a fallback configuration?
Transaction broadcasting should be treated as a distinct subsystem where you query for the transaction hash or receipt before any rebroadcast to avoid duplicate or ambiguous submissions.
Why is transport ranking insufficient for critical application actions?
Transport ranking is a performance metric based on latency and stability, but it does not prove that the data returned is correct or consistent across the chain.

By Caleb North