Your dApp backend reads and writes chain state through a hosted RPC provider. How do you make that dependency reliable?
dApp RPC node reliability means treating blockchain nodes as rate-limited, sometimes lagging read replicas rather than perfect infrastructure. Production backends need retries, confirmation depth, bounded log queries, nonce coordination and their own indexed reads.
6 min readUpdated 2026-08-15
0
Answer
Short answer
Make a dApp backend reliable by using multiple RPC providers, timeouts, backoff, confirmation-depth rules and idempotent retry paths. Do not use raw RPC as the product database; index events and chain state you need to query repeatedly.
Was this answer helpful?
Likely follow-ups
→What does the finalised block tag let you stop worrying about, and what does it cost you?
→How would you make a backfill of two years of contract events survive a provider's log-range cap?
→Why does a full node return an error for a state query that an archive node answers?
→What breaks in a websocket subscription design that polling for new heads would survive?
An RPC endpoint is not a load balancer in front of your own service. It is a read replica of a global database, operated by someone else, whose contents depend on which client software is running, how much history that node retains, how far behind the chain head it currently is, and how much of its capacity your API key is allowed. Every design decision below follows from taking that literally.
The first move in the interview is to split the dependency in two. Reads are idempotent, cacheable, and can be served by any node that has the data — so their reliability problem is availability and correctness of the answer. Writes carry a nonce belonging to one of your accounts, and are therefore stateful in a way that makes naive failover dangerous. Candidates who propose "two providers behind a round-robin" as a single answer have not made this split, and it is the first thing to probe.
The failure modes that are not outages
An outage is the easy case, because it is loud. The expensive failures are the ones that return a 200.
Failure
How it presents
What it costs you
Rate limit or quota exhaustion
Errors under load, often only at peak
Cascading retries make it worse
Log query range cap
A request for a wide block range is rejected
Backfills silently stop partway
Pruned state
Historical state query errors while recent ones work
Analytics wrong only for old data
Node lagging the head
Valid, stale answers
Reads contradict a write you just made
Reorganisation
A block and its receipts cease to exist
Actions taken on discarded events
Provider-specific behaviour
Two providers differ on an edge case
Failover changes your results
The lag case deserves emphasis because it produces the bug people spend longest on. You submit a transaction, it confirms, you immediately read the resulting balance from a pool of nodes, and one member of the pool has not seen that block yet. The read is not wrong from the node's point of view. Your code assumed a single consistent view where there is none, and the fix is either to pin a session's reads to one endpoint or to read at an explicit block number rather than at latest.
Making reads correct rather than merely available
Start by deciding, per read, what you are prepared to act on. Displaying a balance can use the latest block and be occasionally stale. Crediting a user's account, releasing goods, or settling anything must wait for depth: a confirmation count on chains without finality, or on Ethereum the finalized block tag, which tells you the consensus layer will not reorganise past that point. The cost of using it is latency, and naming that trade-off explicitly is a strong answer.
Then design for the reorganisation you will not see. If your ingestion writes an event to your database the moment it appears, you need the inverse operation: store the block hash alongside every derived record, and on each new block check that its parent matches the hash you recorded. When it does not, walk back to the last agreeing block and roll forward again.
Loading diagram…
What to notice is the branch, not the happy path: the rewind exists in the design from the first day, because retrofitting it means auditing every derived table you have already written.
Bound every query. A log query over an unbounded block range will be refused by most providers, so page it — a fixed window per request, a durable cursor, and the ability to resume — and expect to halve the window and retry when a busy range exceeds a result cap. Cache aggressively, because immutable history is the most cacheable data there is: a receipt from a finalised block will never change, so it can be cached forever, while a latest read cannot be cached at all.
Making writes safe rather than merely retried
The nonce is what makes the write path different. One account's transactions form a strict sequence, so the component that signs must be the single authority for that account's next nonce, and it must persist what it allocated before broadcasting. Retrying a submission across two providers is safe — the same signed bytes are the same transaction, and a duplicate is rejected harmlessly. Re-signing after a timeout is not safe, because you may now have two distinct transactions competing for one nonce, and the one you did not intend may be the one that lands.
So the write path wants: a durable record written before broadcast, submission to more than one endpoint for the same signed payload, a watcher that resolves each record to a receipt, and a fee-bumping replacement at the same nonce when a transaction stalls. Under contention, separate accounts for independent workstreams remove the queuing problem entirely, because a stuck transaction on one account no longer blocks the others.
Why a second provider is not automatically a fallback
The instinct is to add a second provider and call the dependency solved. It helps with outages and does nothing for three of the failures above, because the two endpoints are not interchangeable. They may run different client software, retain different amounts of history, enforce different limits, differ on how a failed eth_call reports its revert reason, and be at different heights at the moment you switch. Failover therefore changes the answers you get, which is the thing you were trying to protect.
Making it genuinely useful takes work: health checks that compare block height and not just reachability, a hedge that only crosses to the second endpoint for requests both can serve identically, reads pinned per session so a single logical operation does not straddle two views, and contract tests asserting that both providers agree on a fixed historical block. Run those tests continuously, because a provider changing behaviour is not an event you get told about.
Then ask whether the node should be answering the question at all. "Every transfer for this address, paginated, sorted, filtered" is a query, and a node is a poor query engine. Ingest events once into your own store — or use an indexing layer built for it — and serve the application from there. That converts a rate-limited external dependency into an internal one you can scale, cache and reason about, and it leaves the node responsible for the two things only it can do: telling you the current head and accepting your transactions.
Reads fail by being plausibly wrong and writes fail by being duplicated, so an RPC strategy that treats both with the same failover is protecting the case that would have been obvious anyway.