Use cases
How eRPC works
AI agents: fetch https://docs.erpc.cloud/use-cases/how-it-works.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: /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:

  1. Static responses — matched against network.cfg.StaticResponses before any upstream is contacted.
  2. Multiplexing — identical in-flight requests share one leader goroutine; followers wait and receive a copied response with their original request ID patched in.
  3. Cache readcacheDal.Get fans out across all matching policies; first hit short-circuits the rest of the pipeline.
  4. Upstream orderingpolicyEngine.GetOrdered returns upstreams ranked by selection policy scores; falls back to registry order on cold start.
  5. EVM network pre-forward hook (HandleNetworkPreForward) — upstream-aware short-circuits for eth_getLogs range enforcement, eth_chainId, trace_filter.
  6. Future-block short-circuit — requests for a concrete block beyond every eligible upstream head return null immediately without touching any upstream.
  7. Method guardeth_accounts and eth_sign are hardcoded unsupported; stateful methods require exactly one targeted upstream.
  8. Network rate limiting — network-level budget checked.
  9. Request preparationevm.NormalizeHttpJsonRpc normalizes params (block tag → concrete hex interpolation, EVM-specific transformations), caches block number for downstream availability checks.
  10. Failsafe executor selectiongetFailsafeExecutors iterates failsafe[] in config order, returning the first whose matchMethod (wildcard) and matchFinality match.

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 is ErrDynamicTimeoutExceeded. Can be a static duration or an adaptive quantile function over observed per-method latency.
  • Consensus is delegated to the consensusRunner interface. The executor does not import the consensus package directly.
  • Retry (runRetry) iterates up to maxAttempts. For data-unavailable retries (block_unavailable, empty_result, missing_data) it uses an EMA block-time-relative delay (falling back to emptyResultDelay). For genuine errors it uses exponential backoff via failsafe.ComputeBackoff. firstInformativeErr is tracked so a bare ErrNoUpstreamsLeftToSelect on a later attempt does not mask the root cause.
  • Hedge (runHedge) fires up to maxCount parallel 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:

  1. Calls req.NextUpstream() — atomic round-robin with skip logic for consumed and permanently errored upstreams; safe for concurrent hedge goroutines.
  2. Block availability gating — compares the request's block number against the upstream's EvmEffectiveLatestBlock. Retryable if within maxRetryableBlockDistance (default 128) of the upstream head; fail-open on poller errors.
  3. Calls HandleUpstreamPreForward then u.Forward.
  4. HandleUpstreamPostForward validates the response (integrity checks, mark-empty-as-error methods, eth_sendRawTransaction idempotency).
  5. normalizeResponse rewrites the JSON-RPC id to match the client's original byte-for-byte (preserving large 64-bit integers via IDRawBytes()).
  6. MarkUpstreamCompleted releases 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 pathTypeDefaultBehavior / footguns
networks[].failsafe[].matchMethodstring"*"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*Durationnil (no timeout)Wraps full executor invocation; cause is ErrDynamicTimeoutExceeded. erpc/network_executor.go:L86-89
networks[].failsafe[].retry.maxAttemptsint1 (no retry)Upper bound on retry iterations. erpc/network_executor.go:L210-213
networks[].failsafe[].retry.emptyResultMaxAttemptsint0 (disabled)Cap for data-unavailable retries independently of maxAttempts.
networks[].failsafe[].retry.emptyResultDelay*DurationnilFixed wait before data-unavailable retry, used before EMA block-time warms up.
networks[].failsafe[].retry.emptyResultAccept[]stringDefaultEmptyResultAccept()Methods for which an empty/null result is NOT retried (e.g. eth_call, eth_getLogs).
networks[].failsafe[].retry.backoffFactorfloatper failsafe.ComputeBackoffExponential backoff multiplier for genuine-error retries.
networks[].failsafe[].retry.backoffMaxDelay*Durationper failsafe.ComputeBackoffMaximum inter-retry delay.
networks[].failsafe[].hedge.maxCountint0 (disabled)Maximum concurrent hedge attempts beyond the primary.
networks[].failsafe[].hedge.delayAdaptiveDurationScalar or quantile+tracker for adaptive timing.
networks[].failsafe[].consensus*ConsensusConfignilEnables consensus; delegates to consensus.Run.
networks[].directiveDefaults.retryEmpty*boolfalseRetry on emptyish upstream responses. common/request.go:L575-578
networks[].directiveDefaults.retryPending*booltrue (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*boolfalsePrevent block tag → hex replacement in outbound params.
networks[].directiveDefaults.skipConsensus*boolfalseBypass consensus; retry+hedge still apply. erpc/network_executor.go:L178-191
networks[].evm.maxRetryableBlockDistance*int64128Blocks ahead of upstream head that are retryable. erpc/networks.go:L1975-1979
projects[].rateLimitBudgetstring""Project-level budget ID; empty = no project rate limiting.
networks[].rateLimitBudgetstring""Network-level budget ID.
networks[].multiplexing.enabledboolfalseDeduplicate 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 pathTypeDefaultBehavior / footguns
evm.chainIdint64required (0 = unset)Used for eth_chainId responses and network ID formation (evm:<chainId>).
evm.fallbackFinalityDepthint641024Depth used when the network cannot determine finality dynamically. A block is considered finalized if latestBlock - blockNumber >= fallbackFinalityDepth.
evm.fallbackStatePollerDebounceDuration5sFallback poll interval for state poller when dynamic block time is unknown.
evm.getLogsMaxAllowedRangeint6430_000Max block range for eth_getLogs before forced splitting. common/defaults.go:L2092-2094
evm.getLogsMaxAllowedAddressesint640 (unlimited)Max address count in eth_getLogs filter.
evm.getLogsMaxAllowedTopicsint640 (unlimited)Max topic count in eth_getLogs filter.
evm.getLogsSplitOnError*booltrueSplit and retry eth_getLogs when upstream returns range-too-large error. common/defaults.go:L2095-2097
evm.getLogsSplitConcurrencyint10Max concurrent sub-requests when splitting eth_getLogs. common/defaults.go:L2098-2100
evm.traceFilterSplitOnError*boolnil (disabled)Split and retry trace_filter/arbtrace_filter on range-too-large. Opt-in required.
evm.traceFilterSplitConcurrencyint10Max concurrent sub-requests when splitting trace_filter. common/defaults.go:L2105-2107
evm.enforceBlockAvailability*boolnil (true in logic)Whether to gate requests based on upstream known block bounds.
evm.markEmptyAsErrorMethods[]string11 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*float640.7Scales EMA block time to derive the state-poller debounce interval.
evm.blockUnavailableDelayMultiplier*float641.0Multiplies 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*boolnil (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.emptyResultConfidenceAvailbilityConfidenceblockHeadConfidence 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.clusterDeltaint640 (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*booltrueDeprecated — migrate to directiveDefaults.enforceHighestBlock.
evm.integrity.enforceGetLogsBlockRange*booltrueDeprecated — migrate to directiveDefaults.enforceGetLogsBlockRange.
evm.integrity.enforceNonNullTaggedBlocks*booltrueDeprecated — 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:

projects[].networks[].failsafe[]
erpc.yaml
failsafe:  - matchMethod: "*"    timeout:      duration: 10s    retry:      maxAttempts: 3      backoffMaxDelay: 1s    hedge:      delay:        quantile: 0.7        min: 100ms        max: 2s      maxCount: 1

2. 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:

projects[].networks[]
erpc.yaml
directiveDefaults:  retryPending: truefailsafe:  - matchMethod: "eth_getTransactionReceipt|eth_getTransactionByHash"    retry:      maxAttempts: 10      emptyResultMaxAttempts: 10      emptyResultDelay: 500ms

3. 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:

projects[].networks[].failsafe[]
erpc.yaml
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:

projects[].networks[]
erpc.yaml
multiplexing:  enabled: true

Request/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). Config directiveDefaults are 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) under server.executionHeaders:

    HeaderWhen presentValue
    X-ERPC-VersionalwayseRPC version string
    X-ERPC-Commitalwaysgit commit SHA
    X-ERPC-Attemptsalwaystotal physical ops (upstream + cache)
    X-ERPC-Upstream-Attemptsalwaysupstream-scope attempt count
    X-ERPC-Upstream-Retriesalwaysupstream-scope retry count
    X-ERPC-Upstream-Hedgesalwaysupstream-scope hedge count
    X-ERPC-Network-Attemptsalwaysnetwork-scope rotation count
    X-ERPC-Network-Retriesalwaysnetwork-scope retry count
    X-ERPC-Network-Hedgesalwaysnetwork-scope hedge count
    X-ERPC-Cache-Attemptsonly if cache exercisedcache-scope attempt count
    X-ERPC-Consensus-Slotsonly if consensus usednumber of consensus slots
    X-ERPC-Consensus-Disputesonly if disputesnumber of disputes
    X-ERPC-Cacheif response availableHIT or MISS
    X-ERPC-Upstreamif upstream responseupstream ID that served it
    X-ERPC-Durationif responsetotal 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-114

    HeaderQuery paramBehavior
    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 matching
    X-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_getLogs block range limit
    X-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 transactionsRoot consistency
    X-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 reasonsshouldRetryWithReason returns one of: execution_exception_retryable (EVM execution exception with retryableTowardNetwork=true), block_unavailable (ErrCodeUpstreamBlockUnavailable, subject to emptyResultMaxAttempts cap), missing_data (ErrCodeEndpointMissingData unless RetryEmpty=false), retryable_error (generic IsRetryableTowardNetwork), empty_result (emptyish response with RetryEmpty=true), pending_tx (tx-lookup methods with RetryPending=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: normalizeResponse uses IDRawBytes() for byte-perfect round-trip of the JSON-RPC id field, preserving large 64-bit integer IDs that cannot be represented as float64. 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 emptyResultDelay as 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: true globally unless you also set emptyResultMaxAttempts; without a cap, methods that legitimately return null (e.g. unindexed blocks on archive nodes) will exhaust maxAttempts retries on every call.
  • Prefer staticResponses for 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

  1. ApplyDirectiveDefaults is called twice — once in RequestProcessor and again defensively in Network.Forward for gRPC callers. The guard ensures only the first call has effect. common/request.go:L563-565
  2. Multiplexer follower race on close: a follower arriving during leader cleanup retries LoadOrStore and becomes the new leader or joins a new follower group. erpc/networks.go:L2017-2025
  3. ErrNoUpstreamsLeftToSelect degeneration on retries: after the first retry round, all upstreams may be marked consumed, hiding the original error. firstInformativeErr is tracked and surfaced in the final ErrFailsafeRetryExceeded wrapping. erpc/network_executor.go:L265-284
  4. Hedge emptyish rejection: a fast null from a lagging upstream does not win the hedge race — in-flight siblings continue until a non-null result arrives. Methods in emptyResultAccept (e.g. eth_call) are exempt. erpc/network_executor.go:L607-624
  5. SkipConsensus falls through to retry+hedge: when skipConsensus=true, the executor skips consensus entirely but all other failsafe policies still apply. erpc/network_executor.go:L178-191
  6. 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
  7. Stateful method enforcement: methods marked Stateful require exactly one targeted upstream. Without a UseUpstream selector, multiple upstreams return ErrNotImplemented. eth_accounts and eth_sign are always unsupported. erpc/networks.go:L2112-2143
  8. Block availability check is fail-open: if EvmAssertBlockAvailability errors (poller issues, partial state), the request proceeds to the upstream rather than being gated. erpc/networks.go:L1949-1959
  9. eth_sendRawTransaction is never wrapped in ErrFailsafeRetryExceeded: execution- reverted responses are surfaced directly — the revert IS the answer, not a retry condition. erpc/network_executor.go:L278-283
  10. 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
  11. Query shim retry safety: a gRPC query stream error is only retried on the next upstream if no page has been emitted (StreamError.PageEmitted gate). Partial-result retries are forbidden. erpc/query_executor.go:L247-253
  12. TranslateToJsonRpcException dominant-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; the data field carries the full eRPC error chain for programmatic detection.
  13. trace_blockdebug_traceBlockByNumber fallback in shim: shimQueryTraces first tries trace_block; if the upstream returns ErrCodeEndpointUnsupported, it retries with debug_traceBlockByNumber?callTracer. If that is also unsupported, returns gRPC Unimplemented. erpc/query_shim.go:L388-420
  14. paginateLogsByBlock never 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
  15. Hash-based projection preserves block hash: ProjectBlockFields never zeros the Hash field even when sel.Hash=false, because the hash is required for cursor semantics in paginated query responses. erpc/query_field_projection.go:L12-15
  16. MarkEmptyAsErrorMethods only fires when RetryEmpty directive is true: even if a method is in the list, conversion to ErrEndpointMissingData is gated on req.Directives().RetryEmpty. Operators must set retryEmpty: true (directive default or per-request header) for this feature to activate. architecture/evm/common.go:L23-25
  17. Custom markEmptyAsErrorMethods completely replaces the default 11-method set — there is no merge. Setting markEmptyAsErrorMethods: ["custom_method"] disables all default methods. An explicitly empty markEmptyAsErrorMethods: [] disables the feature entirely. common/defaults.go:L2110-2112
  18. 200-OK EVM revert detection uses dt[1:11]: in ExtractJsonRpcError, the revert-payload check is dt[1:11] == "0x08c379a0" because jr.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
  19. FullySyncedThreshold = 4: an upstream must return eth_syncing: false four consecutive times before eRPC stops sending syncing-check probes. One syncing=true resets the counter. architecture/evm/evm_state_poller.go:L21

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_retry_attempt_totalcounterproject, network, category, reason, finalityEach network-scope retry attempt
erpc_network_data_unavailable_wait_secondshistogramproject, network, category, reason, finalityDeliberate catch-up delay before data-unavailable retry
erpc_network_hedged_request_totalcounterproject, network, upstream, category, hedgeCount, finality, user, agent_nameHedge attempt fired to a specific upstream
erpc_network_hedge_discards_totalcounterproject, network, upstream, category, attempt, hedge, finality, user, agent_nameHedge request discarded (another leg won)
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_multiplexed_requests_totalcounterproject, network, categoryFollower piggybacked on in-flight leader
erpc_network_static_response_served_totalcounterproject, network, categoryStatic response matched
erpc_upstream_wrong_empty_response_totalcounterproject, vendor, network, upstream, category, finality, user, agent_nameUpstream returned empty while another returned data
erpc_upstream_request_errors_totalcounter(upstream error labels)Upstream skip due to block availability check failure
erpc_network_served_tip_block_numbergaugeproject, network, lane, axisServed-tip block number computed for the network
erpc_network_served_tip_lag_blocksgaugeproject, network, lane, axisDeliberate cushion below the freshest upstream tip
erpc_network_served_tip_upstream_excluded_totalcounterproject, network, upstream, axis, reasonUpstream dropped from served-tip computation (velocity/outlier gate)
erpc_upstream_attempt_outcome_totalcounterproject, network, upstream, category, outcome, is_hedge, is_retry, finalityOne increment per upstream attempt with its terminal outcome

Key OpenTelemetry spans:

  • Network.Forward — top-level; attributes: cache.hit, multiplexed, failsafe.matched_method
  • PolicyEngine.GetOrdered — upstream ordering time
  • Network.forwardAttempt — per-attempt; attributes: execution.attempt, execution.retry, execution.hedge
  • Network.UpstreamLoop — per-upstream iteration; attributes: upstream.id, upstream.latest_block, skipped, skip_reason
  • Network.TryForward — call to doForward per upstream
  • Network.NormalizeResponse — response ID rewrite
  • Network.EnrichStatePoller — state poller suggestion after block response
  • Upstream.PreForwardHook / Upstream.PostForwardHook — EVM method hooks per upstream
  • Project.ForwardProject.PreForwardHookNetwork.PostForwardHook
  • QueryStream.Handle — gRPC query stream top-level span
  • Query.Execute, Query.ResolveQueryBounds, Query.ShimBlocks/Logs/Transactions/Traces/Transfers, Query.ForwardSubrequest
  • Cache.Get / Cache.Set — cache fan-out spans with network.id / upstream.id attributes
  • Evm.ExtractBlockReferenceFromRequest / Evm.ExtractBlockReferenceFromResponse — block-ref resolution detail spans

Source code entry points

Related pages