/why.llms.txt
Why eRPC?
Every RPC provider drops requests, returns stale blocks, and silently serves empty results. eRPC sits in front of all of them and fixes that — automatically.
A request that hits eRPC gets auth-checked, deduplicated if identical calls are already in-flight, served from cache if the data is finalized, and then sent through a failsafe chain that retries, hedges, and falls over to a backup upstream — all before your application sees anything. If a provider has a slow moment, a hedge request races it silently. If it goes dark, retry and failover kick in. A single Prometheus endpoint covers the whole fleet.
The result: provider outages stop reaching your users, historical data is never re-fetched, and you finally know which upstream is slow and why.
Why eRPC — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
The request pipeline runs in a fixed order. An HTTP request arrives at erpc/http_server.go, is parsed (batch requests fan out concurrently), and forwarded through RequestProcessor.ProcessUnary (erpc/request_processor.go:35-67). That function resolves the project, validates the JSON-RPC body, authenticates the consumer, resolves the network, applies DirectiveDefaults, and calls project.Forward.
Project layer (erpc/projects.go): acquires a project-level rate limit permit, fires HandleProjectPreForward (EVM hook for eth_blockNumber, eth_call, eth_chainId, eth_getLogs, trace_filter — may short-circuit with an early response), then delegates to network.Forward.
Network layer (erpc/networks.go:931-1603): in order —
- Static response check — matched entries return immediately without touching any upstream.
- Multiplexing — identical in-flight requests are deduplicated; followers wait for the leader's result.
- Cache read — a non-null hit is returned immediately and closes the multiplexer.
- Upstream ordering — the policy engine scores upstreams on latency, error rate, throttling, and block lag, returning them in score order.
- EVM pre-forward hook — range splitting for
eth_getLogs, shortcircuit foreth_chainId. - Future-block short-circuit —
eth_getBlockByNumberrequests for blocks beyond all upstream heads return a truthfulnullimmediately. - Block-tag normalization —
"latest"and"finalized"are replaced with concrete hex block numbers before the request reaches any upstream. - Failsafe execution — the selected executor runs
timeout(retry(hedge(runUpstreamSweep))), ortimeout(consensus(retry(hedge(tryOneUpstream))))when consensus is enabled. - Async cache write — on a successful non-null response, the result is written to cache in a background goroutine with a 10-second deadline using the application context (not the request context, so client disconnects do not abort the write).
- State poller enrichment — for
eth_getBlockByNumber?latest/finalizedandeth_blockNumber, the returned block number is fed back to the upstream'sEvmStatePoller.
Failsafe nesting order is always fixed regardless of config order:
timeout
└─ [consensus]
└─ retry
└─ hedge
└─ upstream sweepTimeout wraps the entire invocation so a single wall-clock budget governs all retry and hedge attempts. Retry sits outside hedge so a hedge race counts as one retry attempt. Consensus sits outside both because it orchestrates multiple upstream slots, each of which can independently retry and hedge.
Upstream sweep loop (erpc/networks.go:1226-1407): iterates upstreams in score order, skipping permanently errored ones and those blocked by UseUpstream directive filters. For each candidate, block availability is gated — requests for blocks within 128 blocks of the upstream's latest are retryable; beyond that, the upstream is permanently skipped for this request. Winners set Attempts/Retries/Hedges on the response.
Response ID normalization rewrites the JSON-RPC id field byte-for-byte using IDRawBytes() to preserve large 64-bit integers that cannot be round-tripped through float64.
Config schema
The /why page is an overview. Each capability has its own config surface:
| Config area | Canonical page |
|---|---|
| Failsafe (timeout / retry / hedge / consensus) | /config/failsafe |
| Caching policies and storage drivers | /use-cases/cut-costs-and-latency |
| Upstream selection and scoring | /config/projects/selection-policies |
| EVM block tracking and served tip | /reference/evm/block-tracking |
| Multiplexing | networks[].multiplexing.enabled (bool, default false) — /config/projects/networks |
| Rate limiters | /config/rate-limiters |
| Auth | /config/auth |
Worked examples
1. Survive a provider outage with automatic failover. Two upstreams configured; retry with 3 attempts and a hedge after 200ms means a down provider is bypassed within one request cycle, with no client timeout fired:
See /use-cases/survive-provider-outages for a full annotated config.
2. Eliminate re-fetches for finalized data. Cache all finalized blocks to Redis so eth_getBlockByNumber("0x1234567") only hits an upstream once ever. Block-tag normalization ensures eth_getBlockByNumber("latest") is stored under its concrete number, not the "latest" tag:
See /use-cases/cut-costs-and-latency for cache policy configuration.
3. Multi-chain fan-out with per-network failsafe tuning. Each networks[] entry carries its own failsafe[] array, so Ethereum mainnet can use aggressive hedging while a cheaper L2 uses a simple retry-only policy — all in a single eRPC instance.
4. Request deduplication under thundering herd. When many callers request the same eth_getLogs range simultaneously (common in indexer deployments), multiplexing (networks[].multiplexing.enabled: true) collapses them into one upstream call. Every caller receives a copy of the result with their own request ID patched in.
Request/response behavior
- Every response carries
X-ERPC-*diagnostic headers:X-ERPC-Attempts,X-ERPC-Upstream-Retries,X-ERPC-Upstream-Hedges,X-ERPC-Cache(HIT/MISS),X-ERPC-Upstream(winning upstream ID),X-ERPC-Duration. Source:erpc/http_server.go:L1105(opens in a new tab) - HTTP status follows JSON-RPC spec: 200 for all JSON-RPC responses (including errors); transport-level failures get appropriate 4xx/5xx codes.
- Batch requests are dispatched concurrently; all results are re-assembled in order before writing the response.
- Internal requests (state pollers, chainId probes) set
IsInternal=true— they bypass retry, hedge, and circuit breaker policies; only per-attempt timeout applies. - Request directives (
X-ERPC-Use-Upstream,X-ERPC-Skip-Cache-Read,X-ERPC-Retry-Empty, etc.) override networkdirectiveDefaultson a per-request basis. See the directive reference in KB 01.
Best practices
- Start with the use-cases pages — each one maps a concrete operational goal (survive outages, cut costs, low-latency reads) to a minimal working config.
- Enable multiplexing (
networks[].multiplexing.enabled: true) for any network that receives burst traffic or shared infrastructure queries; the deduplication is zero-cost when no duplicates arrive. - Leave
evm.enforceBlockAvailabilityat its default (enabled) — it prevents requests from being sent to upstreams that haven't indexed the requested block yet, turning silent wrong-data scenarios into handled retries. - Pair hedging with rate limiters on paid upstreams — each hedge fires a real request; without a budget cap, a spike can trigger overage charges.
- Use
server.executionHeaders: "summary"in production to reduce per-response header size while keeping attempt/retry/hedge counters; use"all"only for debugging (it emits per-attempt upstream trace strings). - The async cache write uses
appCtx, not the request context — a client disconnect or timeout does NOT abort a cache write in progress. The 10-second limit governs the write itself.
Edge cases & gotchas
ApplyDirectiveDefaultsis a no-op if directives are already set. The HTTP server sets directives beforeNetwork.ForwardcallsApplyDirectiveDefaultsdefensively. A request constructed programmatically (gRPC, internal) without HTTP headers uses the network defaults unchanged. Source:common/request.go:563-565- Multiplexer followers may arrive during leader teardown. The
LoadOrStoreloop retries — a late follower either joins a new leader slot or becomes the new leader itself. Source:erpc/networks.go:2017-2025 "safe"and"pending"block tags are NOT interpolated. eRPC does not track the safe checkpoint or mempool state; these tags pass through to upstreams unchanged. Source:architecture/evm/json_rpc.go:L76-L82- Composite requests skip network-scope retry and hedge. Range-split
eth_getLogssub-requests and query shim sub-requests bypass both policies to prevent exponential sub-request amplification. Source:erpc/network_executor.go:378-380,521-526 ErrNoUpstreamsLeftToSelectmasking on retry. After the first retry round, consumed upstreams may all be marked, producing a bareErrNoUpstreamsLeftToSelectthat loses the root cause. The executor tracksfirstInformativeErrand surfaces it in the finalErrFailsafeRetryExceeded. Source:erpc/network_executor.go:265-284- Cache write panics are recovered. Panics in the async cache-write goroutine are caught and reported via
erpc_unexpected_panic_total{scope="cache-set"}— the process does not crash. Source:erpc/networks.go:1496-1508 - Future-block short-circuit is per-method. Only
eth_getBlockByNumberwith a concrete block number triggers the early-null return. Other methods (e.g.eth_getTransactionReceipt) go through the normal upstream sweep and rely on block-availability gating per upstream.
Observability
| Metric | Type | Labels | When it fires |
|---|---|---|---|
erpc_network_requests_received_total | counter | project, network, category, finality, user, agent_name | Request enters project.Forward |
erpc_network_successful_request_total | counter | project, network, vendor, upstream, category, attempt, finality, emptyish, user, agent_name | project.Forward returns non-error |
erpc_network_failed_request_total | counter | project, network, category, attempt, error, severity, finality, user, agent_name | project.Forward returns error |
erpc_network_request_duration_seconds | histogram | project, network, vendor, upstream, category, finality, user | Per-request total duration (success and error) |
erpc_network_multiplexed_requests_total | counter | project, network, category | Follower piggybacks on in-flight leader |
erpc_network_retry_attempt_total | counter | project, network, category, reason, finality | Each network-scope retry attempt |
erpc_network_hedged_request_total | counter | project, network, upstream, category, hedgeCount, finality, user, agent_name | Hedge attempt fired to a specific upstream |
erpc_network_hedge_winner_total | counter | project, network, upstream, category, finality | Upstream won a hedge race |
erpc_network_timeout_fired_total | counter | project, network, category, finality, scope | Network-scope timeout fired |
erpc_network_static_response_served_total | counter | project, network, category | Static response matched |
Key OTel spans: Network.Forward, Project.Forward, PolicyEngine.GetOrdered, Network.forwardAttempt, Network.UpstreamLoop, Network.TryForward, Cache.Get, Cache.Set. Source: erpc/networks.go, erpc/projects.go, architecture/evm/json_rpc_cache.go.
Source code entry points
erpc/request_processor.go(opens in a new tab) — unified entry point for unary and gRPC stream requests; wires project, auth, and network resolutionerpc/network_executor.go:L70-L200(opens in a new tab) — failsafe policy orchestration; fixed timeout/consensus/retry/hedge nestingerpc/networks.go:L931-L1603(opens in a new tab) —Network.Forward: static check, multiplexing, cache read, upstream ordering, block availability gating, failsafe execution, async cache writeerpc/projects.go(opens in a new tab) — project-level rate limiting, metrics, EVM hook wrapperserpc/http_server.go(opens in a new tab) — HTTP ingress, batch dispatch, directive enrichment,X-ERPC-*response header emissionarchitecture/evm/hooks.go(opens in a new tab) — four EVM hook dispatch entry-points (HandleProjectPreForward, HandleNetworkPreForward, HandleNetworkPostForward, HandleUpstreamPostForward)common/request.go(opens in a new tab) — directive system, upstream round-robin selection, exec state, finality caching
Related pages
- Survive provider outages — the canonical failover use case
- Cut costs and latency — caching and deduplication in practice
- Failsafe overview — timeout, retry, hedge, consensus config
- Selection policies — upstream scoring and ordering
- Rate limiters — budget caps for upstream and project traffic
- Auth — consumer authentication and access control
- Networks config — multiplexing, directiveDefaults, EVM block tracking