Why eRPC?
AI agents: fetch https://docs.erpc.cloud/why.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /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 —

  1. Static response check — matched entries return immediately without touching any upstream.
  2. Multiplexing — identical in-flight requests are deduplicated; followers wait for the leader's result.
  3. Cache read — a non-null hit is returned immediately and closes the multiplexer.
  4. Upstream ordering — the policy engine scores upstreams on latency, error rate, throttling, and block lag, returning them in score order.
  5. EVM pre-forward hook — range splitting for eth_getLogs, shortcircuit for eth_chainId.
  6. Future-block short-circuit — eth_getBlockByNumber requests for blocks beyond all upstream heads return a truthful null immediately.
  7. Block-tag normalization — "latest" and "finalized" are replaced with concrete hex block numbers before the request reaches any upstream.
  8. Failsafe execution — the selected executor runs timeout(retry(hedge(runUpstreamSweep))), or timeout(consensus(retry(hedge(tryOneUpstream)))) when consensus is enabled.
  9. 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).
  10. State poller enrichment — for eth_getBlockByNumber?latest/finalized and eth_blockNumber, the returned block number is fed back to the upstream's EvmStatePoller.

Failsafe nesting order is always fixed regardless of config order:

timeout
  └─ [consensus]
       └─ retry
            └─ hedge
                 └─ upstream sweep

Timeout 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 areaCanonical 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
Multiplexingnetworks[].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 network directiveDefaults on 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.enforceBlockAvailability at 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

  1. ApplyDirectiveDefaults is a no-op if directives are already set. The HTTP server sets directives before Network.Forward calls ApplyDirectiveDefaults defensively. A request constructed programmatically (gRPC, internal) without HTTP headers uses the network defaults unchanged. Source: common/request.go:563-565
  2. Multiplexer followers may arrive during leader teardown. The LoadOrStore loop retries — a late follower either joins a new leader slot or becomes the new leader itself. Source: erpc/networks.go:2017-2025
  3. "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
  4. Composite requests skip network-scope retry and hedge. Range-split eth_getLogs sub-requests and query shim sub-requests bypass both policies to prevent exponential sub-request amplification. Source: erpc/network_executor.go:378-380, 521-526
  5. ErrNoUpstreamsLeftToSelect masking on retry. After the first retry round, consumed upstreams may all be marked, producing a bare ErrNoUpstreamsLeftToSelect that loses the root cause. The executor tracks firstInformativeErr and surfaces it in the final ErrFailsafeRetryExceeded. Source: erpc/network_executor.go:265-284
  6. 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
  7. Future-block short-circuit is per-method. Only eth_getBlockByNumber with 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

MetricTypeLabelsWhen it fires
erpc_network_requests_received_totalcounterproject, network, category, finality, user, agent_nameRequest enters project.Forward
erpc_network_successful_request_totalcounterproject, network, vendor, upstream, category, attempt, finality, emptyish, user, agent_nameproject.Forward returns non-error
erpc_network_failed_request_totalcounterproject, network, category, attempt, error, severity, finality, user, agent_nameproject.Forward returns error
erpc_network_request_duration_secondshistogramproject, network, vendor, upstream, category, finality, userPer-request total duration (success and error)
erpc_network_multiplexed_requests_totalcounterproject, network, categoryFollower piggybacks on in-flight leader
erpc_network_retry_attempt_totalcounterproject, network, category, reason, finalityEach network-scope retry attempt
erpc_network_hedged_request_totalcounterproject, network, upstream, category, hedgeCount, finality, user, agent_nameHedge attempt fired to a specific upstream
erpc_network_hedge_winner_totalcounterproject, network, upstream, category, finalityUpstream won a hedge race
erpc_network_timeout_fired_totalcounterproject, network, category, finality, scopeNetwork-scope timeout fired
erpc_network_static_response_served_totalcounterproject, network, categoryStatic 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

Related pages