Your RPC providers disagree. Fail over without mixing chain states.

Ethereum RPC failover can keep your application available while making its answers inconsistent. One provider returns a balance at the current head, another returns an allowance from an older block, and your backend combines them as if they described one moment.

The endpoints may both be healthy by ordinary HTTP checks. We need a stronger definition of usable for this request. Let’s begin with network identity, then choose a shared chain state for related reads.

Verify the network before scoring speed

Check chain identity when a provider is configured and during operation as appropriate. A fast endpoint on the wrong network should never win a latency-based routing decision.

Track the observed head number and hash, response latency, recent errors and supported methods. Keep provider health separate from whether it can serve the particular historical state your operation requires.

Don’t choose a head only because one provider reports a much larger number. Compare against your trusted chain view and investigate implausible divergence.

Pin related reads to one block

For an operation requiring a coherent snapshot, choose a block reference and use it for the related balance, allowance and contract reads. A block number may still be ambiguous across a reorganisation, so use a hash-aware reference when supported.

EIP-1898 specifies block-hash references for relevant JSON-RPC methods. Check actual support in your clients and providers, including canonicality options. EIP-1898 reference.

This gives you consistency of the chosen read context. It doesn’t guarantee that state remains unchanged when a transaction is later included.

Build the provider-selection rule

The example rejects a wrong-chain endpoint and a lagging endpoint that cannot serve the selected block. Both related reads use the same accepted hash.

import assert from 'node:assert/strict';
const providers=[
  {name:'wrong',chain:2,head:150,hashes:new Set(['h100'])},
  {name:'lagging',chain:1,head:99,hashes:new Set(['h99'])},
  {name:'usable',chain:1,head:101,hashes:new Set(['h100'])},
];
function select(chain,block){
  const p=providers.find(p=>p.chain===chain && p.head>=block.number && p.hashes.has(block.hash));
  if(!p)throw new Error('No coherent provider');return p;
}
const block={number:100,hash:'h100'};
const p=select(1,block);
const reads=['balance','allowance'].map(method=>({provider:p.name,method,blockHash:block.hash}));
assert.equal(p.name,'usable');
assert.equal(new Set(reads.map(r=>r.blockHash)).size,1);
assert.throws(()=>select(1,{number:102,hash:'h102'}));
console.log('PASS: chain identity, block availability and coherent related reads');

The fixture doesn’t run JSON-RPC requests. It tests the application’s selection contract. A live adapter must verify actual block hashes, method support and errors rather than trusting advertised fixture capabilities.

Fail over the operation, not a random field

If one read fails midway, decide whether the alternative provider can serve the same pinned block. If it can, retry that read under the same reference. If it can’t, restart the coherent operation or return a clear unavailable outcome.

Don’t quietly retry with latest because the historical read was inconvenient. That produces a mixed snapshot while preserving the appearance of success.

Keep required and optional reads separate. A price estimate or enrichment field may tolerate a different freshness rule, but label that difference in the operation’s contract.

Batching doesn’t replace a snapshot policy

JSON-RPC batching can reduce request overhead, but a batch isn’t automatically an application transaction. Explicit block references remain important for related state reads.

Geth documents batch requests and their use for grouped RPC calls. Check result IDs and per-call errors rather than assuming responses arrive in the order you sent them. Geth batch reference.

Also bound batch size. One huge request can hit provider limits or hold up small interactive work. Measure the behaviour of your deployed endpoint rather than extrapolating from a local transport test.

Keep writes under a different policy

Broadcasting a signed transaction isn’t the same kind of operation as reading a balance. If a submission times out, preserve its transaction identity and reconcile it. Don’t let read-failover logic create a newly signed replacement automatically.

You may rebroadcast the same signed attempt through another permitted provider under your policy, but keep one operation record and classify each response. Provider disagreement about pool visibility doesn’t by itself establish execution failure.

Make disagreement visible

Record selected block hash, chain ID, provider and result timing with each coherent operation. For a conflict, retain enough evidence to distinguish lag, reorganisation and wrong configuration.

Avoid placing full customer addresses or sensitive transaction content in public dashboards. Use protected correlation where required.

Set alert thresholds for persistent head lag and hash disagreement. A circuit breaker based only on HTTP errors can leave an endpoint that returns stale successful responses in rotation indefinitely.

Test failover with different chain views

Configure fixtures with different heads, wrong chain identity and missing historical state. Fail one related read and check whether the retry preserves the chosen reference.

Then run against controlled nodes or recorded provider responses. Test per-call batch errors and a reorganisation while the operation is in progress.

Start by adding chain ID and a block reference to your RPC operation context. Once all related reads carry that context, failover can preserve meaning instead of merely preserving HTTP success.

Run the example and adapt it

Save the code as 29-coherent-rpc-failover.mjs and run it with Node.js 24 or later:

node 29-coherent-rpc-failover.mjs

The code above includes assertions you can run locally. It tests the local decision logic with controlled inputs; it doesn’t connect to a live provider or establish production performance. Keep that boundary when adapting it to your application.

Technical references