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.
What the interviewer is scoring
- Does the candidate separate read reliability from write reliability instead of proposing one failover for both
- Whether the failure modes named go beyond the provider being down
- That reorganisations are handled by a stated confirmation policy rather than by hope
- Whether nonce ownership is recognised as the reason a write path cannot simply be retried anywhere
- Whether an indexer is proposed for the reads a node is genuinely bad at answering
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.
What you are actually depending on
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.
flowchart TD
N[New block header] --> C{Parent hash matches<br/>our last stored hash}
C -->|yes| A[Apply events and advance cursor]
C -->|no| R[Rewind to last agreeing block]
R --> D[Delete derived rows above it]
D --> B[Refetch and reapply]
A --> F[Mark finalised below the finalised tag]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.
© 2026 Preptima. Originally published at preptima.com.
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?
Related questions
- You want to prove the system survives losing a database, in production, on a Tuesday afternoon. How do you run that without being fired?hardAlso on reliability7 min
- A table has forty million rows, thirty-five million of them soft-deleted, and every query for active rows has got slower. Would a partial index help, and what would you have to be careful about?hardAlso on indexing5 min
- How do you isolate a degraded dependency and halt a cascading failure before thread pool exhaustion takes down the entire microservice ecosystem?hardAlso on reliability2 min
- How do you conduct a post-mortem after a catastrophic Sev1 outage triggered by a junior engineer's mistake, without succumbing to the blame game?hardAlso on reliability2 min