RPC Failover That Actually Works: ordered, weighted and quorum failover compared

By John Sullivan · October 30, 2026 · 19 min read · #rpc failover #rpc reliability #viem #ethers #json-rpc

You added a second RPC endpoint last quarter. Both URLs are in a list, the client moves to the second when the first throws, and nothing has paged you since. That is one form of RPC provider failover. The question we would ask is not whether it works. It is which of three mechanisms you are actually running, because two of them cannot tell a wrong answer from a right one.

This is about blockchain JSON-RPC: the calls a wallet, indexer or backend makes to read chain state. If you came looking for generic remote procedure call — Hadoop's RPC layer, a Cisco ASA failover pair — this is a different problem.

The three mechanisms are ordered failover (a list, a timeout, a retry predicate), weighted failover (the list becomes a pool ranked by a score), and quorum failover (several endpoints must agree before an answer is accepted). Each fixes a different failure. The viem fallback transport and the ethers FallbackProvider each get their own piece; here they are the two clients whose defaults make the difference visible. The piece ends with a decision rule that terminates in a choice.

What 'actually works' means

Three things failover has to survive

The endpoint is down. The endpoint is slow. The endpoint answers, and the answer is wrong.

Ordered and weighted handle the first two. Neither can handle the third, because neither compares answers. They decide when to stop asking one endpoint and start asking another. Quorum is the only one of the three that can catch a wrong answer, and only under conditions worth understanding first.

The failure that costs money is the third one

A well-formed wrong answer looks like a normal response. Four shapes to check for:

  • A stale head. eth_blockNumber returns a number, just an old one.
  • A stale finalized tag. The endpoint answers with a block far behind the tip.
  • Pruned state returning null where you expected a value, instead of an error.
  • A log set that is missing entries for a range you know has them.

Every one passes a health check, because a health check asks whether the endpoint answered. A stalled node keeps answering. What it returns is simply an old number [1].

eth_syncing is the clearest example of a check that lies. The same call returns false in two states that mean opposite things: a node that is fully caught up and at the tip, and a node that has stopped syncing altogether — one that has just started, one that has stalled, one waiting on its consensus client — and so has nothing to report [1].

The dangerous version is not hypothetical. On Chiliz, an endpoint returned block 0 — genesis — as its finalized block, and nothing errored. A wait for finalized.number >= txBlock never completes, and a read at that tag returns genesis state [2].

Ordered failover: a list, a timeout, and a retry predicate

What it fixes

Down and slow, cheaply. Your worst-case latency is the sum of your timeouts, and your request count is something you can reason about. This is the right default for most read paths, not the naive option. It is also what both clients give you out of the box: viem's fallback transport with ranking off, and ethers' FallbackProvider at quorum 1 [3][4].

What it does not fix

A wrong answer from the first healthy endpoint is returned as if it were right. Nothing in the mechanism compares answers, so there is nothing to notice a disagreement against [3][4].

The second trap is stickiness. If the list is evaluated once and never re-evaluated, one bad minute can pin traffic to a worse endpoint for as long as the process lives.

What you change in your code

Four decisions.

  1. The list. Order it by what you know, and know that the order is the policy.
  2. A per-attempt timeout, not a total timeout. One slow endpoint should not consume the whole call's budget.
  3. A retry predicate. Which errors are worth trying the next endpoint for, and which are the chain's answer and must not be retried. A revert is the chain answering you; asking a different node gets the same revert [5].
  4. Whether a failure is permanent or decays. If an endpoint fails once, is it out for the process, or back in after a minute? That decision is circuit breaking, and the decay window is yours.

Both clients ship a predicate you probably have not read. viem's default shouldThrow refuses to fail over on transaction rejections, user rejections, WalletConnect session settlement, execution reverts and CAIP code 5000 — it throws instead of trying the next transport [6]. That predicate is already in your client.

The predicate has to read two layers. A rate limit can arrive as HTTP 429 with a Retry-After header, or as a 200 whose body carries a JSON-RPC error [5]. A predicate that reads only error.code misses the 429; one that reads only the HTTP status misses the 200-with-an-error-body. SwiftNodes' documentation describes the rate-limit response and burst behaviour: 429 with Retry-After: 1 [7], and 502 when the upstream is unreachable [8].

The retry taxonomy on SwiftNodes' blog, dated 2026-08-28, sorts errors by what you should do with them. Errors where the node declined to serve the request — a method it does not offer, a provider-policy refusal, a rate limit — are worth sending elsewhere. Chain answers — reverts, insufficient funds, nonce states — are deterministic, and retrying is "just asking the same question louder" [5]. Copy that shape whatever provider you use, and check the dated page against what your client sees. The full classification is in which JSON-RPC errors are worth retrying.

Weighted failover: the list becomes a pool

What it fixes

A slow endpoint is demoted rather than excluded, so a transient blip does not permanently reorder your list. That distinction is the whole value of the mechanism. An excluded endpoint is a capacity loss you have to notice and undo; a demoted one is still in the pool, still sampled, and comes back on its own.

What ranking actually measures (and what it cannot see)

Ranking is opt-in. viem's rank option defaults to false, so if you pass an array and assume array order is priority order, you are running exactly the mechanism you think you are [3][6]. Turn it on and you get a weighted score: stability 0.7, latency 0.3 [3], over the last 10 samples [3]. The interval defaults to the client's pollingInterval: 4,000 ms when the chain's block time is unknown or at least 8 seconds, and half the block time clamped to 500–4,000 ms otherwise [3][6][9]. The docs' prose says 10 seconds; the parameter table and the source say otherwise. Each sample has a 1,000 ms timeout [3].

Here is what the score cannot see. The default ping is net_listening [3]. A sample succeeds if that call resolves and fails if it throws [6]. net_listening tells you the node is listening, not that it is at the chain tip. A stale-but-responsive endpoint scores full stability, and stability carries 0.7 of the weight, so responsiveness dominates. Ranking will confidently promote a fast, responsive, stale endpoint to first position.

That is the distinction SwiftNodes draws in its own writing: liveness is not freshness [1]. Ranking is a liveness mechanism. It is a good one, and it is not a correctness mechanism.

Two more properties. Ranking reorders the list; it does not remove anything from it. A failing transport is demoted to last, not excluded [6]. And the latency score is relative — one minus mean latency over the slowest sample in the window [6] — so a uniformly slow pool still produces a ranking, and the winner is only the least bad.

What you change in your code

You configure rather than write. Three decisions: the interval, the sample count, and whether latency alone may reorder the pool.

Then you accept a consequence most teams have not: the endpoint serving your traffic can change without your code changing. "Which endpoint returned this" is answerable from your logs, not your config.

If you want ranking to reflect freshness, change the ping. The ping is a function you supply [3]. Point it at something that moves — the block number, or the newest block's timestamp — rather than net_listening.

Quorum failover: the only one that can catch a wrong answer

What it fixes

Disagreement becomes a detectable event. Two endpoints returning different block hashes for the same height is something you can act on; one endpoint returning a stale hash is not. Quorum is the only one of the three mechanisms that compares answers, and comparison is the only way to see a wrong answer you did not already know was wrong.

First, the word. This is not quorum in the Raft or Paxos sense — a majority of a cluster agreeing on a log entry. It counts how many backends have to return the same value before that value is accepted [10].

The trap: a majority can be wrong

Quorum returns the highest-weight result that meets or exceeds the threshold [4]. With equal weights that is a majority; with unequal weights it is not. Say "the highest-weight answer that clears the threshold" unless your weights are equal.

The default threshold is half the total provider weight, rounded up [4]. With two providers at the default weight of 1, that is 1. A two-endpoint FallbackProvider therefore returns the first response to arrive and compares nothing at all [4]. You added a second endpoint and configured nothing, so you have no disagreement detection. You have a faster first response and a fallback for a dead one.

Three endpoints at default weight give a threshold of 2 [4]. Two endpoints sharing a stale view now return the stale answer with a quorum's blessing. Quorum does not make a wrong answer impossible. It makes a wrong answer require agreement.

Endpoints Default quorum One wrong One down
2 1 wrong answer returned, no comparison still works (1 of 1)
3 2 wrong answer returned if 2 share the error still quorate (2 of 2)
4 2 wrong answer returned if 2 share the error still quorate
5 3 wrong answer returned if 3 share the error still quorate

Derived from the default threshold formula in [4].

Two more properties. When the threshold cannot be met, ethers throws a SERVER_ERROR "quorum not met" rather than returning a best guess [4]. It fails loudly, which is the right default and not the only reasonable policy. Errors go into the same tally as successful responses, so a failure that enough backends share can clear the threshold and be thrown [4]. Three endpoints returning the same rate-limit error is agreement.

What you change in your code

Four decisions, and the fourth is the one most implementations never write down.

  1. N endpoints. Three is the minimum that can outvote one wrong answer. Two cannot.
  2. A comparison function. What counts as "the same answer" differs per method: a block hash for a block, a value for a balance, set membership for logs. ethers ships four strategies — exact agreement for calls, balances, receipts and logs; a fuzzy mode for getBlockNumber that counts r-1, r and r+1 as agreeing; a median for gas methods; any-result for pending blocks and broadcasts [4]. Read those before writing your own.
  3. A threshold. It cannot exceed the total provider weight [4].
  4. A policy for when quorum fails. Fail the call, or return the highest-weight answer with a flag the caller can see. ethers' default is to fail [4]. If you choose the flag, something has to read the flag.

The decision rule

The rule below is built from what the two libraries do at defaults, which is the state most readers are in.

Setting viem fallback ethers FallbackProvider
Ordering array order (rank: false) priority (default 1)
Ranking / comparison opt-in, weighted score always compares, by method-specific strategy
Default threshold n/a ceil(total weight / 2)
Two endpoints, default first that answers wins quorum 1 — first that answers wins
Three endpoints, default first that answers wins quorum 2 — two must agree
Sample / dispatch interval client.pollingInterval (4,000 ms on Ethereum) [9] 400 ms stallTimeout
Sample window 10 samples n/a
Retries retryCount 3, after all transports tried n/a (dispatches more providers)
Refuses to fail over on reverts, user rejections, tx rejections, code 5000 n/a

Sources: [3][6][4][9].

Three questions, in order

1. Can you detect a wrong answer yourself? Is the response self-verifying against something you already hold — a receipt checked against a block hash you trust, a balance reconciled against a transfer you sent? If you can verify it, quorum buys you little. If you cannot, quorum is the only mechanism that will tell you.

2. What does a wrong answer cost, versus a slow one? Reads that feed an irreversible action go to quorum. Reads that feed a display, a retry or a cache go to ordered. A wrong balance on a dashboard is a bug report. A wrong balance behind a withdrawal is an incident.

3. Are your endpoints interchangeable or ranked? Interchangeable — same chain, same tier, no preference — goes to weighted, because demotion is what you want. Ranked — a primary you trust and a backup you do not — goes to ordered, because you want the primary used until it fails, not until it is marginally slower.

Failure Ordered Weighted Quorum
Down handled handled handled
Slow handled (timeout) handled (demoted) handled (stallTimeout)
Wrong not detected not detected (ping is net_listening) detected only if a minority

Sources: [3][4].

The last row is the one that decides it. Two of the three mechanisms cannot see a wrong answer at all, so if a wrong answer is expensive for you, the choice is already made.

Where the rule gives the same answer

Most read paths land on ordered. That is not a disappointing answer. Ordered is cheap, predictable, and it is what both clients give you by default [3][4]. The paths that land on quorum are the ones that write, or that credit something a user can see.

Quorum is expensive in a way that is easy to underestimate. A quorum call is at least two requests, often three, and its latency is set by the slowest of the ones that must agree. ethers dispatches further providers after a 400 ms stall by default [4]. Most teams do not need that on every call.

One honest limit: failover for writes is a different problem, and this piece does not solve it. Retrying a send can double-submit. SwiftNodes' API reference says a protected send is relayed once and is never retried or failed over, so it cannot be double-submitted [8]. That is a correctness decision, not a resilience one.

Partial failure: one slow, one wrong, one down

One endpoint slow

Ordered fails over on the timeout. The trap is setting that timeout shorter than the endpoint's normal p99. You then fail over on healthy traffic and get the cost of failover — extra requests, extra latency, a second endpoint's worth of load — with none of the benefit. The timeout is a measurement, not a guess. Measure p99 per endpoint, per method, and set the timeout above it.

Weighted demotes. The slow endpoint keeps answering, its latency score drops, and it moves down the pool. Because the latency score is relative to the slowest sample in the window [6], a pool where everything is slow still produces a ranking. The demotion is meaningful only when there is a faster endpoint to promote.

Quorum dispatches. ethers starts the next provider after a 400 ms stall by default [4]. A slow endpoint does not fail a quorum call. It delays it, and the delay is bounded by the slowest endpoint in the tally. If your threshold is 2 and your slow endpoint is one of the two that must agree, your p99 is now that endpoint's p99.

One endpoint wrong

Ordered returns the wrong answer and never notices. The first healthy endpoint's response is the response, and there is nothing to compare it against [3][4].

Weighted returns the wrong answer and may actively prefer it. The ping is net_listening [3], so a stale endpoint that answers quickly scores stability 1.0 and a good latency score. Ranking can move a wrong endpoint to first position and keep it there.

Quorum catches it only if the wrong answer is a minority. Walk the four shapes through it.

  • A stale head. Three endpoints, one stale, threshold 2: the two fresh endpoints agree and the stale one is outvoted. Two endpoints, one stale, threshold 1: the first response wins, and the stale one may be it.
  • A stale finalized tag. Same arithmetic, and this is the shape that survives every liveness check. An opBNB endpoint was observed with a finalized tag about 3.8 hours old on roughly half its answers, against a healthy baseline of about 40 seconds [2]. Half its answers, so a two-endpoint quorum would have returned it whenever it arrived first. SwiftNodes' post on how far behind 'finalized' actually is across chains is the measured version of this shape.
  • Pruned state returning null. Quorum compares values, so null against a value is a disagreement and gets caught. null against null is agreement, and you get a confident null.
  • A log set missing entries. Caught if the comparison is exact and the two endpoints return different sets. Two endpoints returning the same entries in a different order are a subtler case — check what your comparison does with order before you trust it.

That last one is the general lesson. Quorum is only as good as the comparison function, and the comparison function is per method. The stale head is the one to check first, and SwiftNodes' existing treatment of it is in checking whether an endpoint is at the chain tip.

One endpoint down

All three survive it. The differences are detection time and what you retry.

Ordered: the request fails, the predicate decides, the next endpoint gets the request. Detection is immediate if the connection is refused, and bounded by your timeout if the endpoint is black-holing packets. Retry the request, not the connection. For a read that is safe; for a send it is not, which is why the write case is separate [8].

Weighted: the endpoint's stability score drops to zero and it sorts last [6]. It is still in the pool and still sampled, so it comes back on its own. Detection takes one sampling interval — the client's pollingInterval, 4,000 ms on Ethereum [3][6][9].

Quorum: the arithmetic changes, and this is the part nobody plans for. Three endpoints with one down is still quorate at a threshold of 2 [4]. Two of three with one wrong returns the wrong answer with a majority behind it. And a threshold of 2 with only two endpoints does not give you a majority. It gives you a loud failure on any disagreement, which is sometimes exactly what you want, and is a legitimate configuration worth naming rather than discovering.

One documented example of the gap between "the provider has failover" and "you have failover". During a Polygon Mainnet JSON-RPC partial outage on 22 June 2026, Infura's status page said: "Customers in the Growth and Custom tiers with failover enabled will have their traffic forwarded to our infrastructure partners" [11]. Failover was tier-gated and opt-in. A customer on a lower tier, or on a higher tier who had not enabled it, had no failover during a real outage. The entry sits on Infura's rolling status history feed rather than a permalink, so the date and title are what make it checkable [11].

What to measure before you pick

Measure your own endpoints, not the marketing

Three numbers per endpoint, per method: error rate broken out by error class, p50 and p99 latency, and head lag against a reference. The third catches the failure this piece is about, and it is the one most dashboards omit.

The cheapest freshness check needs no second endpoint. Every block carries a timestamp, so compare the newest one against wall-clock. A newest block that is hours old means the node has fallen behind [1]. For a real reference, compare against something independent — you cannot learn where the chain is from the node you are suspicious of [1].

Comparing endpoints against each other is itself a health signal. SwiftNodes' post on block-tag lag describes a health signal built from comparison: each upstream's tag lag is measured against the others serving the same chain, and outliers are dropped from rotation [2]. Its status page describes health checks every 2 minutes and automatic failover across multiple upstream nodes per chain [12]. A health check on a timer is a liveness signal [1]; the tag-lag comparison is the freshness one.

Test an endpoint before you trust it.

Inject the failure

The only test that tells you whether failover works is taking an endpoint away and watching where traffic goes. Do it in staging, then in production during a quiet window.

  1. Kill one endpoint. Confirm traffic moves, and confirm how long it took. If your timeout is 5 seconds, the answer should be about 5 seconds, not 30.
  2. Make one endpoint slow. Add latency above your timeout and confirm the mechanism does what you configured: ordered fails over, weighted demotes, quorum dispatches after the stall.
  3. Make one endpoint wrong. Return a stale block number, or a finalized tag from an hour ago, and confirm something notices. This is the test almost nobody runs, and the only one that distinguishes quorum from the other two. If nothing notices, you have your answer about which mechanism you are running.

The assertion for the third test is simple: call eth_blockNumber, wait a few seconds, call it again, and check that the number went up [1]. A head that is not advancing is a wrong answer that passes every liveness check.

Where this leaves you

Failover is client-side work. A second provider is necessary and not sufficient: a two-endpoint FallbackProvider at default settings compares nothing [4], and a ranked pool will promote a fast stale endpoint to first [3]. The mechanism should follow from what a wrong answer costs you, not from which strategy sounds most resilient.

If you take one thing from this: identify which mechanism your client is actually running before you change anything. The defaults are not what most people assume, and the difference between ordered and quorum is the difference between a wrong answer you never see and one you can act on.

Compare RPC providers.

Sources

  1. Is Your RPC Node at the Chain Tip? — eth_syncing returns false for two opposite states — SwiftNodes, 2026-08-10
  2. How Old Is 'finalized'? We Measured 62 Chains — wrong-but-well-formed answer — SwiftNodes, 2026-09-30
  3. Fallback Transport — viem (viem.sh), 2026-10-26
  4. ethers.js src.ts/providers/provider-fallback.ts (main) — default quorum — ethers-io/ethers.js, GitHub, 2026-10-27
  5. JSON-RPC Error Codes: Retry, Fix, or Accept? — SwiftNodes, 2026-08-28
  6. viem src/clients/transports/fallback.ts (main) — wevm/viem, GitHub, 2026-10-27
  7. Rate Limits — Rate Limit Response — SwiftNodes, 2026-01-01
  8. API Reference — protect=1 disables retry and failover for sends — SwiftNodes, 2026-01-01
  9. Public Client — pollingInterval default — viem (viem.sh), 2026-10-27
  10. Fallback Provider — API documentation — docs.ethers.org, 2025-04-26
  11. Polygon Mainnet JSON-RPC API partial outage — incident update, 22 Jun 2026 22:52 UTC — Infura (status.infura.io), 2026-06-22
  12. SwiftNodes status page — page description — SwiftNodes, 2026-01-01

This article was researched, written and edited by AI under human supervision, and checked against Rankwrite's public editorial rubric.

J
John Sullivan
Infrastructure Writer, SwiftNodes

John Sullivan covers RPC infrastructure, node operations, and multi-chain development at SwiftNodes — what it actually takes to keep endpoints fast, fresh, and reliable across EVM and non-EVM networks.

Related posts

  • How Old Is 'finalized'? We Measured 62 Chains

    eth_getBlockByNumber('finalized') ranges from instant to four hours across 62 EVM chains; five give no answer, one returns genesis. The table and code.

  • trace_filter and trace_transaction: Beyond Receipts

    Receipts show that a transaction succeeded; traces show what it did. How trace_transaction and trace_filter expose internal calls, where the Parity trace namespace actually works in 2026 (we probe it weekly — it's 11 of 54 EVM chains), and the param quirks that trip people up.

  • JSON-RPC Error Codes: Retry, Fix, or Accept?

    -32601, -32000, code 3, HTTP 429 — every RPC error belongs to one of three families: try another node, fix your request, or accept the chain's answer. A field guide with real error payloads.

Try SwiftNodes free — multi-chain RPC across 83 networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →