/use-cases/how-it-works.llms.txt
How eRPC works
Your JSON-RPC call enters eRPC and immediately gets smarter. It checks a shared cache, races
multiple providers in parallel if one is slow, retries against a fresh upstream on errors, and
reaches consensus across nodes when data fidelity matters — all before a response reaches
your client. Every hop is traced: X-ERPC-* headers tell you exactly what happened, and
Prometheus counters back it up.
Agent reference
Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:
Prompt Example #1: set up a production-ready eRPC config from scratch
I want to set up eRPC in front of my EVM RPC providers so it handles failover, caching, and tail-latency automatically. Walk me through a production-ready my eRPC config that covers the full request lifecycle — auth, cache, retry, hedge, and circuit breaker. Read the reference first: https://docs.erpc.cloud/use-cases/how-it-works.llms.txt
Prompt Example #2: understand and tune an existing eRPC config
Audit my existing my eRPC config and explain what happens to a JSON-RPC call at each stage of the eRPC pipeline — which policies match, what the failsafe chain looks like, and whether any obvious gaps exist (missing timeout, no hedge, etc.). Reference: https://docs.erpc.cloud/use-cases/how-it-works.llms.txt
Prompt Example #3: debug unexpected X-ERPC-* header values
My eRPC responses have unexpected X-ERPC-Attempts and X-ERPC-Hedges values — some calls show 4+ attempts even for simple eth_call requests. Walk me through the pipeline order and explain which stage is likely causing the extra attempts, then adjust my eRPC config to reduce unnecessary retries. Reference: https://docs.erpc.cloud/use-cases/how-it-works.llms.txt
eRPC request lifecycle — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
eRPC routes each JSON-RPC call through a deterministic, layered pipeline. Understanding the pipeline order matters for configuration — earlier stages short-circuit later ones.
1. HTTP ingress (erpc/http_server.go)
The handler parses the URL path for {projectId}/{architecture}/{chainId}, applies aliasing
rules if configured, reads and decompresses the body (gzip supported), and dispatches single
or batch JSON-RPC payloads. Batch items fan out as concurrent goroutines; results reassemble
before the response is written. The real client IP is resolved from trusted forwarder headers
before any auth or rate-limit checks.
2. RequestProcessor (erpc/request_processor.go:35–67)
ProcessUnary is the entry point for both HTTP and gRPC. It resolves the project, wraps raw
JSON bytes in NormalizedRequest, calls Validate() (missing method → immediate error),
authenticates the consumer via project.AuthenticateConsumer, resolves
networkID = "{architecture}:{chainId}", applies directive defaults from
network.Config().DirectiveDefaults, then delegates to project.Forward.
3. Project layer (erpc/projects.go)
PreparedProject.Forward acquires a project-level rate-limit permit (no-op when
rateLimitBudget is unset), fires HandleProjectPreForward — the first EVM hook, which
handles eth_blockNumber (early cache return), eth_call (static call optimisation),
eth_chainId (served from config), and proactive eth_getLogs / trace_filter range
splitting. If the hook handles the request, HandleNetworkPostForward is called on the
result before returning. Otherwise the call proceeds to network.Forward.
4. Network layer (erpc/networks.go:931–1603)
Network.Forward is the core dispatch function, in execution order:
- Static responses — matched against
network.cfg.StaticResponsesbefore any upstream is contacted. - Multiplexing — identical in-flight requests share one leader goroutine; followers wait and receive a copied response with their original request ID patched in.
- Cache read —
cacheDal.Getfans out across all matching policies; first hit short-circuits the rest of the pipeline. - Upstream ordering —
policyEngine.GetOrderedreturns upstreams ranked by selection policy scores; falls back to registry order on cold start. - EVM network pre-forward hook (
HandleNetworkPreForward) — upstream-aware short-circuits foreth_getLogsrange enforcement,eth_chainId,trace_filter. - Future-block short-circuit — requests for a concrete block beyond every eligible
upstream head return
nullimmediately without touching any upstream. - Method guard —
eth_accountsandeth_signare hardcoded unsupported; stateful methods require exactly one targeted upstream. - Network rate limiting — network-level budget checked.
- Request preparation —
evm.NormalizeHttpJsonRpcnormalizes params (block tag → concrete hex interpolation, EVM-specific transformations), caches block number for downstream availability checks. - Failsafe executor selection —
getFailsafeExecutorsiteratesfailsafe[]in config order, returning the first whosematchMethod(wildcard) andmatchFinalitymatch.
5. Failsafe executor chain (erpc/network_executor.go:69–200)
The executor nests policies in a fixed order:
timeout (wraps entire invocation)
↳ if consensus enabled && !SkipConsensus:
consensus(
retry(
hedge(tryOneUpstream) ← one upstream per consensus slot
)
)
else:
retry(
hedge(runUpstreamSweep) ← all upstreams per sweep
)- Timeout wraps the context with
context.WithTimeoutCause; the cause isErrDynamicTimeoutExceeded. Can be a static duration or an adaptive quantile function over observed per-method latency. - Consensus is delegated to the
consensusRunnerinterface. The executor does not import the consensus package directly. - Retry (
runRetry) iterates up tomaxAttempts. For data-unavailable retries (block_unavailable,empty_result,missing_data) it uses an EMA block-time-relative delay (falling back toemptyResultDelay). For genuine errors it uses exponential backoff viafailsafe.ComputeBackoff.firstInformativeErris tracked so a bareErrNoUpstreamsLeftToSelecton a later attempt does not mask the root cause. - Hedge (
runHedge) fires up tomaxCountparallel copies after an adaptive delay. Write methods are never hedged. A hedge leg returning an emptyish result does not win the race — siblings continue until a non-null response arrives.
6. Upstream sweep loop (erpc/networks.go:1226–1407)
For each iteration inside sweepFn:
- Calls
req.NextUpstream()— atomic round-robin with skip logic for consumed and permanently errored upstreams; safe for concurrent hedge goroutines. - Block availability gating — compares the request's block number against the
upstream's
EvmEffectiveLatestBlock. Retryable if withinmaxRetryableBlockDistance(default 128) of the upstream head; fail-open on poller errors. - Calls
HandleUpstreamPreForwardthenu.Forward. HandleUpstreamPostForwardvalidates the response (integrity checks, mark-empty-as-error methods,eth_sendRawTransactionidempotency).normalizeResponserewrites the JSON-RPCidto match the client's original byte-for-byte (preserving large 64-bit integers viaIDRawBytes()).MarkUpstreamCompletedreleases retryable-errored upstreams back into rotation for the next failsafe retry round. Deterministic client errors return immediately.
7. Response write and async cache
Back in http_server.go, setResponseHeaders emits the full X-ERPC-* diagnostic surface
(attempts, retries, hedges, cache status, winning upstream, duration). The JSON-RPC response
body is streamed. An async goroutine fires cacheDal.Set with a 10-second deadline under
appCtx — a client disconnect never aborts the cache write. Panics in the write goroutine
are recovered and reported via erpc_unexpected_panic_total{scope="cache-set"}.
gRPC query-stream path
ProcessQueryStream handles eth_queryBlocks, eth_queryTransactions, eth_queryLogs,
eth_queryTraces, and eth_queryTransfers. Auth and rate limiting are identical to the
unary path. EvmQueryExecutor.Execute attempts native pipe-through to an upstream gRPC BDS
client; if no upstream supports structured queries, it shims via sequential JSON-RPC
subrequests (eth_getBlockByNumber, eth_getLogs, trace_block, etc.) with block-boundary-
aware pagination. Composite subrequests set IsCompositeRequest=true to skip network-scope
retry and hedge, preventing exponential amplification.
Config schema
Config fields governing the lifecycle. Network-level failsafe and directive defaults are the primary control surface.
| YAML path | Type | Default | Behavior / footguns |
|---|---|---|---|
networks[].failsafe[].matchMethod | string | "*" | Wildcard pattern; first matching executor wins. erpc/network_executor.go:L83-84 |
networks[].failsafe[].matchFinality | []DataFinalityState | [] (any) | Empty = match all finalities. erpc/network_executor.go:L79 |
networks[].failsafe[].timeout | *Duration | nil (no timeout) | Wraps full executor invocation; cause is ErrDynamicTimeoutExceeded. erpc/network_executor.go:L86-89 |
networks[].failsafe[].retry.maxAttempts | int | 1 (no retry) | Upper bound on retry iterations. erpc/network_executor.go:L210-213 |
networks[].failsafe[].retry.emptyResultMaxAttempts | int | 0 (disabled) | Cap for data-unavailable retries independently of maxAttempts. |
networks[].failsafe[].retry.emptyResultDelay | *Duration | nil | Fixed wait before data-unavailable retry, used before EMA block-time warms up. |
networks[].failsafe[].retry.emptyResultAccept | []string | DefaultEmptyResultAccept() | Methods for which an empty/null result is NOT retried (e.g. eth_call, eth_getLogs). |
networks[].failsafe[].retry.backoffFactor | float | per failsafe.ComputeBackoff | Exponential backoff multiplier for genuine-error retries. |
networks[].failsafe[].retry.backoffMaxDelay | *Duration | per failsafe.ComputeBackoff | Maximum inter-retry delay. |
networks[].failsafe[].hedge.maxCount | int | 0 (disabled) | Maximum concurrent hedge attempts beyond the primary. |
networks[].failsafe[].hedge.delay | AdaptiveDuration | — | Scalar or quantile+tracker for adaptive timing. |
networks[].failsafe[].consensus | *ConsensusConfig | nil | Enables consensus; delegates to consensus.Run. |
networks[].directiveDefaults.retryEmpty | *bool | false | Retry on emptyish upstream responses. common/request.go:L575-578 |
networks[].directiveDefaults.retryPending | *bool | true (implicit) | Retry tx-lookup methods until confirmed. common/request.go:L579-583 |
networks[].directiveDefaults.skipCacheRead | *string | "" (off) | "true" = skip all; connector-id pattern = skip matching. |
networks[].directiveDefaults.useUpstream | *string | "" | Upstream id or glob; applied inside NextUpstream loop. |
networks[].directiveDefaults.skipInterpolation | *bool | false | Prevent block tag → hex replacement in outbound params. |
networks[].directiveDefaults.skipConsensus | *bool | false | Bypass consensus; retry+hedge still apply. erpc/network_executor.go:L178-191 |
networks[].evm.maxRetryableBlockDistance | *int64 | 128 | Blocks ahead of upstream head that are retryable. erpc/networks.go:L1975-1979 |
projects[].rateLimitBudget | string | "" | Project-level budget ID; empty = no project rate limiting. |
networks[].rateLimitBudget | string | "" | Network-level budget ID. |
networks[].multiplexing.enabled | bool | false | Deduplicate identical in-flight requests. erpc/networks.go:L1990 |
networks[].staticResponses | []StaticResponseConfig | [] | Matched before cache and upstream; zero upstream contact. |
server.executionHeaders | *ExecutionHeadersMode | "all" | "all" = full per-attempt trace; "summary" = counters only; "off" = none. |
EVM network config — all under networks[].evm. in YAML; defaults set by EvmNetworkConfig.SetDefaults() (common/defaults.go):
| YAML path | Type | Default | Behavior / footguns |
|---|---|---|---|
evm.chainId | int64 | required (0 = unset) | Used for eth_chainId responses and network ID formation (evm:<chainId>). |
evm.fallbackFinalityDepth | int64 | 1024 | Depth used when the network cannot determine finality dynamically. A block is considered finalized if latestBlock - blockNumber >= fallbackFinalityDepth. |
evm.fallbackStatePollerDebounce | Duration | 5s | Fallback poll interval for state poller when dynamic block time is unknown. |
evm.getLogsMaxAllowedRange | int64 | 30_000 | Max block range for eth_getLogs before forced splitting. common/defaults.go:L2092-2094 |
evm.getLogsMaxAllowedAddresses | int64 | 0 (unlimited) | Max address count in eth_getLogs filter. |
evm.getLogsMaxAllowedTopics | int64 | 0 (unlimited) | Max topic count in eth_getLogs filter. |
evm.getLogsSplitOnError | *bool | true | Split and retry eth_getLogs when upstream returns range-too-large error. common/defaults.go:L2095-2097 |
evm.getLogsSplitConcurrency | int | 10 | Max concurrent sub-requests when splitting eth_getLogs. common/defaults.go:L2098-2100 |
evm.traceFilterSplitOnError | *bool | nil (disabled) | Split and retry trace_filter/arbtrace_filter on range-too-large. Opt-in required. |
evm.traceFilterSplitConcurrency | int | 10 | Max concurrent sub-requests when splitting trace_filter. common/defaults.go:L2105-2107 |
evm.enforceBlockAvailability | *bool | nil (true in logic) | Whether to gate requests based on upstream known block bounds. |
evm.markEmptyAsErrorMethods | []string | 11 methods (see below) | Methods for which an empty/null upstream result is converted to ErrEndpointMissingData and retried. Default set: eth_blockNumber, eth_getBlockByNumber, eth_getTransactionByHash, eth_getTransactionByBlockHashAndIndex, eth_getTransactionByBlockNumberAndIndex, eth_getUncleByBlockHashAndIndex, eth_getUncleByBlockNumberAndIndex, debug_traceTransaction, trace_transaction, trace_block, trace_get. eth_getBlockByHash, eth_getTransactionReceipt, eth_getBlockReceipts are intentionally excluded. common/defaults.go:L2044-2058 |
evm.dynamicBlockTimeDebounceMultiplier | *float64 | 0.7 | Scales EMA block time to derive the state-poller debounce interval. |
evm.blockUnavailableDelayMultiplier | *float64 | 1.0 | Multiplies EMA-estimated block time to derive the dynamic retry delay for ErrUpstreamBlockUnavailable/ErrEndpointMissingData. Returns 0 before EMA warms up — falls back to failsafe[*].retry.emptyResultDelay. |
evm.idempotentTransactionBroadcast | *bool | nil (enabled) | When enabled, eth_sendRawTransaction converts "already known" to success and verifies "nonce too low" via eth_getTransactionByHash. Set to false to return raw upstream errors. |
evm.emptyResultConfidence | AvailbilityConfidence | blockHead | Confidence level for empty-result retries: blockHead = retry empties for blocks ≤ latest head; finalizedBlock = stricter, only retry for blocks ≤ finalized head. common/defaults.go:L2075-2079 |
evm.servedTip.enabledFor | []string | [] | Tags using cluster-min mode; valid values: "latest", "finalized", "safe". |
evm.servedTip.clusterDelta | int64 | 0 (auto-derived, clamped [2,10]) | Max block gap to group upstreams into one cluster for served-tip computation. |
evm.servedTip.guaranteedMethods | []string | [] | Glob patterns for methods whose supporting-upstreams subset is used for cluster computation. |
evm.integrity.enforceHighestBlock | *bool | true | Deprecated — migrate to directiveDefaults.enforceHighestBlock. |
evm.integrity.enforceGetLogsBlockRange | *bool | true | Deprecated — migrate to directiveDefaults.enforceGetLogsBlockRange. |
evm.integrity.enforceNonNullTaggedBlocks | *bool | true | Deprecated — migrate to directiveDefaults.enforceNonNullTaggedBlocks. |
Worked examples
1. Baseline resilient config — timeout + retry + hedge for any method. Good starting point for a production network with multiple upstreams. The timeout bounds the whole race; retry recovers transient upstream errors; hedge cuts tail latency on slow providers:
failsafe: - matchMethod: "*" timeout: duration: 10s retry: maxAttempts: 3 backoffMaxDelay: 1s hedge: delay: quantile: 0.7 min: 100ms max: 2s maxCount: 12. Slow-data retry for pending transactions. eth_getTransactionReceipt and
eth_getTransactionByHash return null until the transaction is mined. Combine
emptyResultMaxAttempts with the directive default retryPending: true to poll until
confirmed, using block-time-relative delays:
directiveDefaults: retryPending: truefailsafe: - matchMethod: "eth_getTransactionReceipt|eth_getTransactionByHash" retry: maxAttempts: 10 emptyResultMaxAttempts: 10 emptyResultDelay: 500ms3. Consensus for critical state reads. Use consensus when multiple upstreams must agree
on a result before it is returned. skipConsensus directive lets callers opt out per-request:
failsafe: - matchMethod: "eth_getBalance|eth_call" consensus: minParticipants: 2 method: "majority"4. Multiplexing for high-traffic hot paths. When many clients request the same block simultaneously, multiplexing deduplicates the upstream request — one upstream call serves all waiting followers with a response ID patch per follower:
multiplexing: enabled: trueRequest/response behavior
-
Directive precedence: HTTP headers → query params (query params override headers for
use-upstream,retry-empty,retry-pending,skip-cache-read,skip-interpolation,skip-consensus). ConfigdirectiveDefaultsare applied only when directives are not already set — HTTP-set directives are never overwritten.common/request.go:L563-565 -
Response headers emitted by
setResponseHeaders(erpc/http_server.go:1105) underserver.executionHeaders:Header When present Value X-ERPC-Versionalways eRPC version string X-ERPC-Commitalways git commit SHA X-ERPC-Attemptsalways total physical ops (upstream + cache) X-ERPC-Upstream-Attemptsalways upstream-scope attempt count X-ERPC-Upstream-Retriesalways upstream-scope retry count X-ERPC-Upstream-Hedgesalways upstream-scope hedge count X-ERPC-Network-Attemptsalways network-scope rotation count X-ERPC-Network-Retriesalways network-scope retry count X-ERPC-Network-Hedgesalways network-scope hedge count X-ERPC-Cache-Attemptsonly if cache exercised cache-scope attempt count X-ERPC-Consensus-Slotsonly if consensus used number of consensus slots X-ERPC-Consensus-Disputesonly if disputes number of disputes X-ERPC-Cacheif response available HITorMISSX-ERPC-Upstreamif upstream response upstream ID that served it X-ERPC-Durationif response total request duration in milliseconds X-ERPC-UpstreamsexecutionHeaders=allper-attempt trace: id=role:outcome:durationMs:won|lost -
Request directives — settable via HTTP header or query param (query overrides header for the non-validation directives). Header-only validation directives (
X-ERPC-Validate-*,X-ERPC-Enforce-*) are not settable via query params.common/request.go:L38-114Header Query param Behavior X-ERPC-Retry-Emptyretry-emptyRetry emptyish upstream responses X-ERPC-Retry-Pendingretry-pendingRetry pending tx lookups X-ERPC-Skip-Cache-Readskip-cache-read"true"= skip all; connector-id pattern = skip matchingX-ERPC-Use-Upstreamuse-upstreamPin request to matching upstream(s) by id, glob, or tag X-ERPC-Skip-Interpolationskip-interpolationDon't replace block tags with hex in outbound params X-ERPC-Skip-Consensusskip-consensusBypass consensus; retry+hedge still apply X-ERPC-Enforce-Highest-Blockenforce-highest-blockBlock integrity validation X-ERPC-Enforce-GetLogs-Rangeenforce-getlogs-rangeEnforce eth_getLogsblock range limitX-ERPC-Enforce-Non-Null-Tagged-Blocksenforce-non-null-tagged-blocksReject null on tagged block lookups X-ERPC-Enforce-Log-Index-Strict-Incrementsenforce-log-index-strict-incrementsValidate log index ordering X-ERPC-Validate-Logs-Bloom-Emptinessvalidate-logs-bloom-emptinessBloom↔logs consistency check X-ERPC-Validate-Logs-Bloom-Matchvalidate-logs-bloom-matchRecalculate bloom from logs X-ERPC-Validate-Tx-Hash-Uniquenessvalidate-tx-hash-uniquenessNo duplicate tx hashes in block X-ERPC-Validate-Transaction-Indexvalidate-transaction-indexValidate tx positions X-ERPC-Receipts-Count-Exactreceipts-count-exactExpected exact receipt count X-ERPC-Receipts-Count-At-Leastreceipts-count-at-leastMinimum receipt count X-ERPC-Validation-Expected-Block-Hashvalidation-expected-block-hashExpected block hash ground truth X-ERPC-Validation-Expected-Block-Numbervalidation-expected-block-numberExpected block number ground truth X-ERPC-Validate-Transactions-Rootvalidate-transactions-rootCheck transactionsRootconsistencyX-ERPC-Validate-Header-Field-Lengthsvalidate-header-field-lengthsValidate EVM header field byte lengths X-ERPC-Validate-Transaction-Fieldsvalidate-transaction-fieldsValidate transaction fields X-ERPC-Validate-Transaction-Block-Infovalidate-transaction-block-infoValidate tx block info X-ERPC-Validate-Log-Fieldsvalidate-log-fieldsValidate log fields -
Retry reasons —
shouldRetryWithReasonreturns one of:execution_exception_retryable(EVM execution exception withretryableTowardNetwork=true),block_unavailable(ErrCodeUpstreamBlockUnavailable, subject toemptyResultMaxAttemptscap),missing_data(ErrCodeEndpointMissingDataunlessRetryEmpty=false),retryable_error(genericIsRetryableTowardNetwork),empty_result(emptyish response withRetryEmpty=true),pending_tx(tx-lookup methods withRetryPending=true). Composite requests are never retried at network scope.erpc/network_executor.go:L374-463 -
Batch requests fan out to per-item goroutines; each item is processed independently through the full pipeline and reassembled before writing.
-
Composite subrequests (
IsCompositeRequest=true) skip network-scope retry and hedge to prevent exponential amplification of range-split sub-requests.erpc/network_executor.go:L378-380 -
Response ID fidelity:
normalizeResponseusesIDRawBytes()for byte-perfect round-trip of the JSON-RPCidfield, preserving large 64-bit integer IDs that cannot be represented asfloat64.erpc/networks.go:L2234-2266
Best practices
- Set a timeout for every failsafe block in production; without it, a hung upstream holds the goroutine indefinitely and can exhaust the connection pool under load.
- Use adaptive hedge delay (
quantile: 0.7,min: 100ms,max: 2s) over static delays — static delays go stale as provider latencies shift; quantile mode self-adjusts per method. - Enable multiplexing for networks that see bursts of identical calls (e.g.
eth_blockNumber,eth_getBlockByNumber?latest); it eliminates duplicate upstream cost at zero config complexity. - Set
emptyResultDelayas a bootstrap fallback for data-unavailable retries — the EMA block-time delay takes a few requests to warm up; without a fallback the first retries fire immediately. - Do not set
directiveDefaults.retryEmpty: trueglobally unless you also setemptyResultMaxAttempts; without a cap, methods that legitimately return null (e.g. unindexed blocks on archive nodes) will exhaustmaxAttemptsretries on every call. - Prefer
staticResponsesfor methods your upstreams do not support and will never support (e.g. a custom method returning a fixed value); this avoids wasting upstream budget on guaranteed errors. - Internal requests (state pollers, chainId probes) set
IsInternal=true, bypassing retry, hedge, and circuit breaker — only per-attempt timeout applies. Never forward internal requests to a public consumer-facing failsafe path.
Edge cases & gotchas
ApplyDirectiveDefaultsis called twice — once inRequestProcessorand again defensively inNetwork.Forwardfor gRPC callers. The guard ensures only the first call has effect.common/request.go:L563-565- Multiplexer follower race on close: a follower arriving during leader cleanup retries
LoadOrStoreand becomes the new leader or joins a new follower group.erpc/networks.go:L2017-2025 ErrNoUpstreamsLeftToSelectdegeneration on retries: after the first retry round, all upstreams may be marked consumed, hiding the original error.firstInformativeErris tracked and surfaced in the finalErrFailsafeRetryExceededwrapping.erpc/network_executor.go:L265-284- Hedge emptyish rejection: a fast
nullfrom a lagging upstream does not win the hedge race — in-flight siblings continue until a non-null result arrives. Methods inemptyResultAccept(e.g.eth_call) are exempt.erpc/network_executor.go:L607-624 SkipConsensusfalls through to retry+hedge: whenskipConsensus=true, the executor skips consensus entirely but all other failsafe policies still apply.erpc/network_executor.go:L178-191- Cache write panic recovery: panics in the async write goroutine are recovered and
reported via
erpc_unexpected_panic_total{scope="cache-set"}.erpc/networks.go:L1496-1508 - Stateful method enforcement: methods marked
Statefulrequire exactly one targeted upstream. Without aUseUpstreamselector, multiple upstreams returnErrNotImplemented.eth_accountsandeth_signare always unsupported.erpc/networks.go:L2112-2143 - Block availability check is fail-open: if
EvmAssertBlockAvailabilityerrors (poller issues, partial state), the request proceeds to the upstream rather than being gated.erpc/networks.go:L1949-1959 eth_sendRawTransactionis never wrapped inErrFailsafeRetryExceeded: execution- reverted responses are surfaced directly — the revert IS the answer, not a retry condition.erpc/network_executor.go:L278-283- Async cache write does not block the response: the write goroutine uses
appCtx, not the request context, so a client disconnect never aborts the write. The 10-second deadline is on the write itself, not on waiting for it.erpc/networks.go:L1494-1518 - Query shim retry safety: a gRPC query stream error is only retried on the next upstream
if no page has been emitted (
StreamError.PageEmittedgate). Partial-result retries are forbidden.erpc/query_executor.go:L247-253 TranslateToJsonRpcExceptiondominant-code selection: when all upstreams return skipped or unsupported errors, the wire JSON-RPC code depends on the first child's error type — either-32601(method not found) or-32603(server error). Clients cannot reliably distinguish "globally unsupported" from "internal error" by code alone; thedatafield carries the full eRPC error chain for programmatic detection.trace_block→debug_traceBlockByNumberfallback in shim:shimQueryTracesfirst triestrace_block; if the upstream returnsErrCodeEndpointUnsupported, it retries withdebug_traceBlockByNumber?callTracer. If that is also unsupported, returns gRPCUnimplemented.erpc/query_shim.go:L388-420paginateLogsByBlocknever splits a block across pages: when adding a block's logs would exceed the page limit and the page already has items, the function stops before that block (producing a cursor). If the first block alone exceeds the limit, all its logs are included — the limit is a soft cap at block boundaries, not an absolute log count.erpc/query_shim.go:L617-649- Hash-based projection preserves block hash:
ProjectBlockFieldsnever zeros theHashfield even whensel.Hash=false, because the hash is required for cursor semantics in paginated query responses.erpc/query_field_projection.go:L12-15 MarkEmptyAsErrorMethodsonly fires whenRetryEmptydirective is true: even if a method is in the list, conversion toErrEndpointMissingDatais gated onreq.Directives().RetryEmpty. Operators must setretryEmpty: true(directive default or per-request header) for this feature to activate.architecture/evm/common.go:L23-25- Custom
markEmptyAsErrorMethodscompletely replaces the default 11-method set — there is no merge. SettingmarkEmptyAsErrorMethods: ["custom_method"]disables all default methods. An explicitly emptymarkEmptyAsErrorMethods: []disables the feature entirely.common/defaults.go:L2110-2112 - 200-OK EVM revert detection uses
dt[1:11]: inExtractJsonRpcError, the revert-payload check isdt[1:11] == "0x08c379a0"becausejr.GetResultString()returns the JSON-encoded value —dt[0]is the JSON double-quote character, so the actual hex string starts at index 1. A check starting at index 0 always fails on valid JSON strings.architecture/evm/error_normalizer.go:L609-624 FullySyncedThreshold = 4: an upstream must returneth_syncing: falsefour consecutive times before eRPC stops sending syncing-check probes. Onesyncing=trueresets the counter.architecture/evm/evm_state_poller.go:L21
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_retry_attempt_total | counter | project, network, category, reason, finality | Each network-scope retry attempt |
erpc_network_data_unavailable_wait_seconds | histogram | project, network, category, reason, finality | Deliberate catch-up delay before data-unavailable retry |
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_discards_total | counter | project, network, upstream, category, attempt, hedge, finality, user, agent_name | Hedge request discarded (another leg won) |
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_multiplexed_requests_total | counter | project, network, category | Follower piggybacked on in-flight leader |
erpc_network_static_response_served_total | counter | project, network, category | Static response matched |
erpc_upstream_wrong_empty_response_total | counter | project, vendor, network, upstream, category, finality, user, agent_name | Upstream returned empty while another returned data |
erpc_upstream_request_errors_total | counter | (upstream error labels) | Upstream skip due to block availability check failure |
erpc_network_served_tip_block_number | gauge | project, network, lane, axis | Served-tip block number computed for the network |
erpc_network_served_tip_lag_blocks | gauge | project, network, lane, axis | Deliberate cushion below the freshest upstream tip |
erpc_network_served_tip_upstream_excluded_total | counter | project, network, upstream, axis, reason | Upstream dropped from served-tip computation (velocity/outlier gate) |
erpc_upstream_attempt_outcome_total | counter | project, network, upstream, category, outcome, is_hedge, is_retry, finality | One increment per upstream attempt with its terminal outcome |
Key OpenTelemetry spans:
Network.Forward— top-level; attributes:cache.hit,multiplexed,failsafe.matched_methodPolicyEngine.GetOrdered— upstream ordering timeNetwork.forwardAttempt— per-attempt; attributes:execution.attempt,execution.retry,execution.hedgeNetwork.UpstreamLoop— per-upstream iteration; attributes:upstream.id,upstream.latest_block,skipped,skip_reasonNetwork.TryForward— call todoForwardper upstreamNetwork.NormalizeResponse— response ID rewriteNetwork.EnrichStatePoller— state poller suggestion after block responseUpstream.PreForwardHook/Upstream.PostForwardHook— EVM method hooks per upstreamProject.Forward→Project.PreForwardHook→Network.PostForwardHookQueryStream.Handle— gRPC query stream top-level spanQuery.Execute,Query.ResolveQueryBounds,Query.ShimBlocks/Logs/Transactions/Traces/Transfers,Query.ForwardSubrequestCache.Get/Cache.Set— cache fan-out spans withnetwork.id/upstream.idattributesEvm.ExtractBlockReferenceFromRequest/Evm.ExtractBlockReferenceFromResponse— block-ref resolution detail spans
Source code entry points
erpc/request_processor.go(opens in a new tab) —RequestProcessor.ProcessUnary/ProcessQueryStream: project resolution, auth, network selection, unified entry point for HTTP and gRPC.erpc/networks.go(opens in a new tab) —Network.Forward: multiplexing, cache read/write, upstream ordering, block-availability gating, all EVM hooks,normalizeResponse,enrichStatePoller.erpc/network_executor.go(opens in a new tab) —networkExecutor.Run: failsafe policy composition (timeout → consensus → retry → hedge); retry reason classification and backoff computation.erpc/projects.go(opens in a new tab) —PreparedProject.Forward: project-level rate limiting, metrics, shadow upstreams, EVM hook wrapper.erpc/http_server.go(opens in a new tab) — HTTP ingress, batch dispatch, directive enrichment,setResponseHeaders.architecture/evm/hooks.go(opens in a new tab) — Four EVM lifecycle hooks:HandleProjectPreForward,HandleNetworkPreForward,HandleNetworkPostForward,HandleUpstreamPostForward.common/request.go(opens in a new tab) —NormalizedRequest: directive system,NextUpstreamround-robin,MarkUpstreamCompleted, composite type, finality caching,ExecState.erpc/query_executor.go(opens in a new tab) —EvmQueryExecutor.Execute: structured query dispatch; native pipe-through vs. shim routing.
Related pages
- Survive provider outages — retry, hedge, and circuit breaker in depth.
- Cut costs & latency — cache policies, TTLs, and storage drivers.
- Scale chains & providers — selection policies, scoring, and shadow upstreams.
- Trust the data — consensus, integrity checks, and getLogs splitting.
- Lock it down — auth strategies, rate limiters, CORS.
- See everything — Prometheus metrics, OTel spans, dashboards.
- Retry — retry policy config reference.
- Hedge — hedge policy config reference.
- Rate limiters — budget and rule configuration.