Config
Networks
AI agents: fetch https://docs.erpc.cloud/config/projects/networks.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: /config/projects/networks.llms.txt

Networks

A network is one chain inside a project. Declare it once and eRPC owns the whole pipeline: smarter caching, automatic failover, and in-flight deduplication — on every chain you touch. Three things make it ergonomic: zero-config chains bootstrap automatically from your providers on first request, networkDefaults lets one block set shared policy for every chain, and a human-readable alias replaces evm/42161 in every URL.

What you get:

  • Any EVM chain or SVM (Solana) cluster, any provider — routes automatically on first request.
  • One networkDefaults block replaces repetitive per-chain config.
  • alias: arbitrum turns every URL into /main/arbitrum instead of /main/evm/42161.
  • Static canned replies for chains with non-standard genesis blocks.
  • In-flight deduplication so 100 identical concurrent requests cost you one upstream call.

Quick taste

Illustrative, not a tuned production config — declare Ethereum with a human-readable alias:

projects[].networks[]
erpc.yaml
projects:  - id: main    networks:      - architecture: evm        evm:          chainId: 1        # human-readable alias replaces evm/1 in every URL and metric label        alias: ethereum

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: add multiple chains with shared policy
I'm running eRPC for several EVM chains and want to declare Ethereum mainnet,
Arbitrum, and Base in a single project. Set human-readable aliases (e.g. "ethereum",
"arbitrum", "base"), share a common rate-limit budget and failsafe via networkDefaults,
and keep per-chain blocks minimal. Work with my existing eRPC config. Read the full reference
first: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #2: tune getLogs limits and splitting
My eRPC instance is hitting "block range too large" errors from upstreams on
eth_getLogs. Adjust getLogsMaxAllowedRange for my Ethereum mainnet network, enable
automatic range splitting on errors, and set a safe concurrency cap so sub-requests
don't flood the provider. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #3: fix backward tip jumps in multi-pod deployment
My clients occasionally see older block numbers between requests, which I think
means different eRPC pods are returning different "latest" tips. Enable cluster-min
served tip for the "latest" axis on my production chains so all pods agree on the
served tip. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #4: disable validation for a non-standard chain
I'm adding zkSync Era to eRPC and eth_getBlockByNumber responses from zkSync use a
non-standard transactionsRoot — the data-integrity module is rejecting them. For the
zkSync network only, make sure data-integrity is off (or disable the transactionsRoot
checks via the integrity block) without touching the global policy. Work with my existing
eRPC config. Reference: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #5: debug 503 on a newly added chain
I added a new chain to eRPC's networks list but requests are returning HTTP 503
with ErrNetworkInitializing. Walk me through what the bootstrap task does, what
conditions produce 503 vs 404, and which config keys to check first (providers,
onlyNetworks/ignoreNetworks, rateLimitBudget). Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/projects/networks.llms.txt
Networks — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How it works

Architecture concept. NetworkArchitecture is a string enum with two valid values: "evm" and "svm" (common/network.go:L14-17). The canonical id is evm:<chainId> from util.EvmNetworkId, or svm:<cluster> / svm:<chain>:<cluster> from util.SvmNetworkId (util/ids.go:L1-30). The network Prometheus label equals the alias when set, otherwise the raw id.

architecture is inferred when omitted: a network carrying an svm block derives architecture: svm, one carrying evm (or neither) derives evm (common/defaults.go:L2010-2028). Declaring architecture: svm auto-creates an empty svm block, and networkDefaults.evm is never injected into an SVM network. See SvmNetworkConfig below and the SVM reference pages.

Lazy creation. NetworksRegistry.GetNetwork fast-paths a sync.Map lookup; on miss it validates the id format and schedules a bootstrap task. The task: (1) resolves or synthesizes a NetworkConfig and runs SetDefaults so it inherits networkDefaults exactly like static networks; (2) calls PrepareUpstreamsForNetwork — fans out to every configured provider, polls 200 ms for up to 30 s until ≥1 upstream is ready; (3) wires the cache DAL; (4) registers with the selection-policy engine. Statically-declared networks bootstrap in background at startup through the same machinery; a failed eager bootstrap is retried when the first request arrives.

Forward pipeline order (exact sequence, erpc/networks.go:L931-1603):

  1. Apply directiveDefaults + start OTel span.
  2. Static response match — returns immediately if hit (before multiplexer, cache, upstreams).
  3. Multiplexer — leader/follower on CacheHash; followers copy the leader's response.
  4. Cache read — non-null hit closes the mux and returns.
  5. Policy-engine upstream ordering — falls back to registration order if empty; ErrNoUpstreamsFound (404) if still empty.
  6. EVM pre-forward hook (future-block short-circuit; never cached).
  7. Stateful-method guard, network rate-limit, request normalization.
  8. Failsafe executor selection — first config-order entry matching method + finality.
  9. Upstream sweep — per-upstream block-availability gate, hedge/retry orchestration.
  10. Post-sweep: timeout translation, last-valid-response fallback, async cache set, misbehavior accounting.

networkDefaults inheritance (common/defaults.go:L1781-1973). "Network wins; defaults fill gaps." Scalars inherit when zero-valued; pointers when nil. selectionPolicy and directiveDefaults are whole-struct copies (no per-field merge). evm is a whole-struct copy when absent from the network, otherwise per-field fill for 14 named fields. Failsafe: if the network has entries, each is matched against the first compatible default (wildcard method + finality) and missing sub-fields are inherited; if the network has no entries the defaults list is deep-copied wholesale.

Finality classification (erpc/networks.go:L1645-1743). Explicit finalized/realtime flags in methods.definitions win. Otherwise the block ref/number is extracted from the request, then from the response body (for hash-keyed cache hits). Non-numeric tags → realtime; numeric blocks are checked via EvmIsBlockFinalized on the serving upstream, then the last upstream tried, then the network-wide lowest-finalized heuristic.

Served tip. Default ("max mode"): EvmHighestLatestBlockNumber / EvmHighestFinalizedBlockNumber return the MAX across policy-eligible, non-syncing upstreams. Setting evm.servedTip.enabledFor switches that axis to a cluster-min, cross-pod monotonic counter. Rollback tolerance is 1024 blocks (DefaultToleratedBlockHeadRollback, architecture/evm/evm_state_poller.go:L27 (opens in a new tab)). A use-upstream selector produces a per-group scoped tip keyed by the SHA-256 (first 8 bytes, hex-encoded) of the sorted matched upstream-id set; unmatched selectors compute a stateless min and emit no gauges. Group partitions are capped at 16 (maxServedTipPartitions); a "simple" group selector must be ≤128 chars, no whitespace, at most one leading !, and the matched set must be ≥2 upstreams and less than all upstreams. Syncing upstreams are excluded from every head computation (max mode, cluster mode, lowest-finalized, and guaranteed-method floors). EvmLeaderUpstream returns the upstream with the highest raw LatestBlock(); EvmLowestFinalizedBlockNumber returns the min positive effective finalized across non-syncing upstreams.

Config schema

projects[].networks[] — NetworkConfig

FieldTypeDefaultBehavior / footguns
architecturestring (evm | svm)Inferred from the sub-block: svm present → "svm", otherwise "evm" (common/defaults.go:L2010-2028)Both evm and svm are valid (common/network.go:L82-88). Required by validation.
evmEvmNetworkConfigAuto-created empty struct for EVM networks (common/defaults.go:L1914-1916); never injected into an SVM networkSee EvmNetworkConfig table below.
svmSvmNetworkConfigAuto-created empty struct when architecture: svm (common/defaults.go:L2026-2028)Required for SVM networks — network.*.svm is required for svm networks otherwise. See SvmNetworkConfig table below.
aliasstring""Charset [a-zA-Z0-9_-]+ (common/validation.go:L1264-1269); unique per project (common/validation.go:L622-627). Used as the network metric label via Network.Label() (erpc/networks.go:L278-286). Aliases work in URL path only — not in body networkId.
rateLimitBudgetstring"" (none); inherits networkDefaults.rateLimitBudget when empty (common/defaults.go:L1783-1785)Must reference an existing budget. Enforced per-request before upstream contact → ErrNetworkRateLimitRuleExceeded.
failsafe[][]FailsafeConfignil; inherited/merged from networkDefaults.failsafeOne networkExecutor per entry + a no-op catch-all (erpc/networks_registry.go:L115-138). Selection: first config-order match on matchMethod wildcard + matchFinality (erpc/networks.go:L907-929). Old single-object YAML auto-converted to a one-element list with matchMethod: "*".
selectionPolicySelectionPolicyConfignil; inherited wholesale from networkDefaults.selectionPolicy when nil (common/defaults.go:L1833-1836)Auto-attached when any upstream has tag tier:fallback (common/defaults.go:L1932-1940). See Selection & scoring.
directiveDefaultsDirectiveDefaultsConfigAlways materialized (common/defaults.go:L1947-1950); inherited wholesale from networkDefaults.directiveDefaults when nil (common/defaults.go:L1837-1840)Footgun: a network that sets ANY directiveDefaults field ignores networkDefaults.directiveDefaults entirely — no per-field merge. Applied at request start (erpc/networks.go:L937).
multiplexing*boolnil = enabled (common/config.go:L2034-2039); inherits networkDefaults.multiplexing when nil (common/defaults.go:L1841-1844)Gates in-flight identical-request dedup. Footgun: legacy single-object networkDefaults.failsafe YAML drops this field silently (old struct has no Multiplexing field, common/config.go:L626-655).
staticResponses[][]StaticResponseConfignilChecked before multiplexer, cache, upstreams (erpc/networks.go:L976-981). See Static responses.
staticResponses[].methodstringrequiredExact JSON-RPC method name; case-sensitive string equality (common/validation.go:L1282-1284).
staticResponses[].params[]anynilnil and [] are interchangeable in matching (both have len==0). YAML params: [] deserializes to non-nil empty slice; omitted params: deserializes to nil — both match requests with zero params. Hex strings ("0x0" vs "0x00") are NOT normalized — exact string match only. Declaration order matters: first match wins.
staticResponses[].response.resultanyExactly one of result or error must be set; both or neither → startup error.
staticResponses[].response.error.codeint0No minimum/maximum enforced. Footgun: code 0 passes validation but is omitted from the wire JSON (json:"code,omitempty" on int, common/errors.go:L2226). Always set a non-zero code in production.
staticResponses[].response.error.messagestringrequired when error setNon-empty string required; Validate() returns "response.error.message is required" otherwise.
staticResponses[].response.error.dataanynilOptional arbitrary extra data; omitted from JSON when nil.
methods.preserveDefaultMethodsboolfalseCritical footgun: false + any definitions entries → ALL built-in methods (hundreds, including eth_call, eth_getLogs) are silently replaced by only your custom entries plus stateful markers. false + no definitions → all built-ins kept (same as omitting the block). true + any definitions → built-ins copied first, then user entries merged on top (common/defaults.go:L493-576).
methods.definitionsmap[string]CacheMethodConfigFull built-in table (common/defaults.go:L493-528)Per-method fields: finalized, realtime, stateful, reqRefs, respRefs, translateLatestTag, translateFinalizedTag, enforceBlockAvailability.
methods.definitions.<m>.finalizedboolfalse (true for static-cache methods)Footgun: setting true on a non-finalized method (e.g. eth_getBalance) permanently caches ALL responses from that method with long/permanent TTL — a severe data-correctness bug (erpc/networks.go:L1656-1660).
methods.definitions.<m>.realtimeboolfalse (true for eth_blockNumber and similar)Forces GetFinalityRealtime; responses use the realtime cache policy (short TTL).
methods.definitions.<m>.statefulbooltrue for DefaultStatefulMethodNames, else falseFootgun: stateful: false on any of the 6 built-in stateful methods (eth_newFilter, eth_newBlockFilter, eth_newPendingTransactionFilter, eth_getFilterChanges, eth_getFilterLogs, eth_uninstallFilter) is silently overridden to true with no warning (common/defaults.go:L512-519).
methods.definitions.<m>.translateLatestTag*boolnil = enablednil/true"latest" tag replaced with current block hex before forwarding and cache keying. false → tag passed through unchanged. eth_getBlockByNumber ships with false so it can discover new blocks. Footgun: false on a changing method means all callers share one "latest" cache key; stale data returned until TTL expires (common/config.go:L307-309).
methods.definitions.<m>.translateFinalizedTag*boolnil = enabledSame as translateLatestTag for the "finalized" tag (common/config.go:L310-312).
methods.definitions.<m>.enforceBlockAvailability*boolnil (inherit)Per-method override; highest priority in the 5-step enforcement chain (erpc/networks.go:L1806-1813).

projects[].networks[].evm — EvmNetworkConfig

FieldTypeDefaultNotes
chainIdint64— (no default)Drives networkId = "evm:<chainId>". Not validated > 0 for networks; chainId: 0 yields evm:0.
fallbackFinalityDepthint641024 (common/defaults.go:L1975)Used when an upstream doesn't expose the finalized tag. Must be > 0 after defaults.
fallbackStatePollerDebounceDuration5s (common/defaults.go:L1976)Static debounce for block polling until dynamic block time is learned. Must be > 0.
dynamicBlockTimeDebounceMultiplier*float640.7 (common/defaults.go:L1977)Polling debounce = EMA block time × multiplier. Lower → more aggressive polling (fresher data, more upstream load).
blockUnavailableDelayMultiplier*float641.0 (common/defaults.go:L1978)Retry delay for block-unavailable = EMA block time × multiplier.
enforceBlockAvailability*boolnil (defer; fallback = enabled)Network-level priority 2 in 5-step chain. nil defers to upstream bounds and system defaults. false disables enforcement even when an upstream has explicit bounds configured.
maxRetryableBlockDistance*int64nil128 at use site (erpc/networks.go:L1973-1979)Block-unavailable errors within this distance of upstream head are retryable; beyond → non-retryable skip.
getLogsMaxAllowedRangeint6430_000 (common/defaults.go:L2092-2094)Hard limit on eth_getLogs block range. Must be > 0; 0 inherits defaults value.
getLogsMaxAllowedAddressesint640 = unlimitedEnforced only when > 0 (architecture/evm/eth_getLogs.go:L212).
getLogsMaxAllowedTopicsint640 = unlimitedEnforced only when > 0.
getLogsSplitOnError*booltrue (common/defaults.go:L2095-2097)Retry by bisecting range on "too many results" errors.
getLogsSplitConcurrencyint10 (common/defaults.go:L2098-2100)Parallelism cap for split sub-requests.
traceFilterSplitOnError*boolnil = off (deliberate opt-in, common/defaults.go:L2102-2104)Opt-in bisecting for trace_filter/arbtrace_filter.
traceFilterSplitConcurrencyint10 (common/defaults.go:L2105-2107)Parallelism cap for trace-filter splits.
idempotentTransactionBroadcast*boolnil = enabled (architecture/evm/eth_sendRawTransaction.go:L27-36)"Already known"/"nonce too low"-verified errors become success-with-tx-hash, making retry/hedge safe for eth_sendRawTransaction.
markEmptyAsErrorMethods[]stringeth_blockNumber, eth_getBlockByNumber, eth_getTransactionByHash, and 8 others (common/defaults.go:L2044-2057)Methods where empty response = error (retried, upstream scored down). eth_getTransactionReceipt deliberately excluded.
emptyResultConfidenceblockHead | finalizedBlockblockHead (common/defaults.go:L2075-2079)How confirmed a block must be for empty point-lookups to be retried as missing data.
safeBlockSourcestring (upstream selector)"" = inherit or provider-defined routingRoutes standard block-tagged JSON-RPC requests carrying safe to matching upstreams and skips cache reads. The tag stays verbatim so those upstreams define both height and hash. Operator routing overrides client use-upstream; retry/consensus still applies inside the matching pool. An empty network value inherits networkDefaults.evm.safeBlockSource when present. Does not affect eth_query* or direct gRPC Query APIs.
evm.integrity.enforceHighestBlock*booltrue (common/defaults.go:L2117-2120)Deprecated — migrated into directiveDefaults.enforceHighestBlock at SetDefaults time when the directive is unset (common/defaults.go:L1952-1966). Prefer directiveDefaults.enforceHighestBlock.
evm.integrity.enforceGetLogsBlockRange*booltrue (common/defaults.go:L2121-2123)Deprecated — same migration path as enforceHighestBlock.
evm.integrity.enforceNonNullTaggedBlocks*booltrue (common/defaults.go:L2124-2126)Deprecated — same migration path.
maxFutureBlockRetryDistance*int64nilDeprecated — warned about and set to nil in SetDefaults (common/defaults.go:L2080-2083). Has no effect; remove from configs.
servedTipEvmServedTipConfignil = max modeCopied wholesale from networkDefaults.evm.servedTip when nil.
servedTip.enabledFor[]string[] (max mode)Valid: latest, finalized, safe. Listing a tag switches that axis from max-across-upstreams to cluster-min + monotonic clamp via shared state.
servedTip.clusterDeltaint640 = auto-derive from EMA block time, clamped [2, 10]Must be ≥ 0.
servedTip.guaranteedMethods[]string (glob)[]Global tip clamped to per-method supporting-set floor (erpc/networks.go:L643-649).

projects[].networks[].svm — SvmNetworkConfig

FieldTypeDefaultBehavior / footguns
clusterstring"" (required)The cluster these upstreams serve — mainnet-beta, devnet, testnet, or a fork's own cluster name. Together with chain it forms the network id. This is network identity (the SVM analogue of evm.chainId) and is therefore not inherited from networkDefaults.svm.
chainstring"" → treated as "solana"Which SVM chain this network runs on. Set it explicitly for forks and variants (fogo, eclipse) so several SVM chains can be hosted side by side without network-id or cache-key collisions. Inherited from networkDefaults.svm.chain.
commitmentstring""no defaultOne of finalized, confirmed, processed. When unset nothing is injected and each upstream's own server-side default governs (Solana's is finalized), so upstreams can disagree on identical requests. Setting it pins one level across the pool so the cache and consensus compare like-for-like; note that it also makes finality classification track the configured level. Details: Commitment & finality.
statePollerDebounceDuration400ms (one slot) (common/defaults.go:L2237-2243)Minimum interval between poll fan-outs of an upstream's slot/health view. A gate, not the ticker period — the ticker stays pinned at 400 ms, so values below it have no effect. Raising it widens the getBlock availability guard's staleness margin proportionally.
maxFinalizedSlotLag*int64100 slots ≈ 40 s (common/defaults.go:L2244-2251)Bounds how far an upstream's finalized slot may trail the pool reference before it is excluded from consensus voting on finalized data. Applied only when a consensus policy is active AND the request's resolved finality is Finalized. Tri-state: omitted → 100; explicit 0 → filter disabled; >0 → that value. The field is a pointer precisely so an explicit 0 is distinguishable from "unset" — a plain integer would collapse the two and make the documented disable switch unreachable.
enforceBlockAvailability*boolniltrue (materialized at read time by SvmEnforceBlockAvailability(), erpc/networks.go:L704-708)Gates the getBlock/getConfirmedBlock guard that short-circuits requests for slots above the pool's highest indexed slot, saving quota and firing the indexing-lag retry immediately. Set false when shred-insert tracking is unavailable or unreliable on a deployment. An explicit false in networkDefaults.svm is honored.

Not present on SvmNetworkConfig: there is no maxSlotsPerSignaturesQuery. An early design proposed one as an analogue of EVM's getLogs range cap, but getSignaturesForAddress is bounded by signature cursors (before/until), not by a slot range, so the key described semantics Solana does not have and was removed. minContextSlot is a node-freshness floor — the minimum bank slot at which a request may be evaluated — not a lower bound on returned history, so it cannot serve as a range cap either.

Cluster and genesis-hash validation. Known (chain, cluster) pairs — currently Solana mainnet-beta, devnet, and testnet — have their genesis hash validated at bootstrap by a single getGenesisHash call compared against a hardcoded table. Both a mismatch and a fetch failure fail the upstream, so a node mis-pointed at the wrong cluster never joins the pool. For unknown clusters (forks, private clusters) the same check runs only when the upstream sets svm.checkGenesisHash: true. getGenesisHash itself is then answered from the table with no upstream round-trip, mirroring EVM's eth_chainId short-circuit.

projects[].networks[].directiveDefaults — DirectiveDefaultsConfig

FieldTypeDefault
retryEmpty*boolnil
retryPending*boolnil
skipCacheReadbool|stringnil
useUpstream*stringnil
skipInterpolation*boolnil
skipConsensus*boolnil
enforceHighestBlock*booltrue (common/defaults.go:L1458-1460)
enforceGetLogsBlockRange*booltrue (common/defaults.go:L1461-1463)
enforceNonNullTaggedBlocks*booltrue (common/defaults.go:L1464-1466)

Data-integrity validation is configured under networks[].integrity (not directiveDefaults) — an opt-in level/checks/profiles block. The deprecated per-check flags (validateLogsBloomMatch, enforceLogIndexStrictIncrements, validateTransactionsRoot, …) are translated into it at startup. See Integrity checks.

projects[].networkDefaults — NetworkDefaults

FieldTypeDefaultInheritance rule
rateLimitBudgetstring""Copied when network's value is empty string (common/defaults.go:L1783-1785). Validated after inheritance per network — an invalid name surfaces as a per-network error (e.g. "network.*.rateLimitBudget 'x' does not exist"), not a top-level networkDefaults error. Networks that explicitly set their own non-empty rateLimitBudget are not affected.
failsafe[][]FailsafeConfignilNetwork has none → deep-copied wholesale. Network has some → per-entry merge from the FIRST compatible default (wildcard method + finality match); break on first match (common/defaults.go:L1793-1832).
selectionPolicySelectionPolicyConfignilShallow-copied when network's is nil (common/defaults.go:L1833-1836).
directiveDefaultsDirectiveDefaultsConfignilShallow-copied when network's is nil — no per-field merge (common/defaults.go:L1837-1840).
evmEvmNetworkConfignilStruct-copied wholesale when network has no evm block — except onto SVM networks, which never receive it. Otherwise per-field fill for: integrity, fallbackStatePollerDebounce, dynamicBlockTimeDebounceMultiplier, blockUnavailableDelayMultiplier, fallbackFinalityDepth, getLogsMaxAllowed*, getLogs*, traceFilter*, servedTip, emptyResultConfidence. NOT inherited individually: chainId, enforceBlockAvailability, maxRetryableBlockDistance, markEmptyAsErrorMethods, idempotentTransactionBroadcast (common/defaults.go:L1845-1889).
svmSvmNetworkConfignilPer-field fill: chain, commitment, statePollerDebounce, maxFinalizedSlotLag, enforceBlockAvailability (common/defaults.go:L2197-2221). cluster is deliberately NOT inherited — it is network identity, and inheriting it would let two networks alias one another. The two pointer fields are tested for nil, not for their zero value, so an explicit maxFinalizedSlotLag: 0 or enforceBlockAvailability: false set here survives the merge.
multiplexing*boolnilValue-copied when network's multiplexing is nil (common/defaults.go:L1841-1844). No deep merge — a network must set its own multiplexing to deviate from the project-wide default.

Note: networkDefaults has no alias, methods, staticResponses, or architecture fields — those are per-network only.

server.aliasing.rules[] — domain-based aliasing

FieldTypeDefaultBehavior
rules[]listnil; auto-created {matchDomain: "*", serveProject: "main"} when config has zero projects (common/defaults.go:L100-110)First rule whose matchDomain wildcard matches the request Host wins (erpc/http_server.go:L232-257).
rules[].matchDomainstring (wildcard)Matched against Host header (port stripped).
rules[].serveProjectstringPre-selects projectId.
rules[].serveArchitecturestringPre-selects architecture.
rules[].serveChainstringPre-selects chainId. Pre-selecting project+chain WITHOUT architecture is rejected at parse time.

Worked examples

All patterns below are distilled from real production fleets; comments explain the non-obvious choices.

1. The production baseline: networkDefaults with finality-split failsafe policies. Block-availability races on realtime/unfinalized requests need a non-zero retry delay; finalized/unknown don't — this is why production splits them:

projects[].networkDefaults
erpc.yaml
projects:  - id: main    networkDefaults:      # All chains inherit these unless they set their own failsafe list      directiveDefaults:        retryEmpty: true        retryPending: false      # multiplexing: false in production to avoid follower-timeout amplification      # on chains where every block is a write (e.g. high-throughput L2s)      multiplexing: false      evm:        getLogsMaxAllowedRange: 30000        getLogsSplitOnError: true        # cluster-min tip on "latest" prevents backward tip jumps across pods        servedTip:          enabledFor: [latest]      failsafe:        # NOTE: delay MUST NOT be 0 for realtime/unfinalized — a short non-zero        # delay gives shared-state propagation time to catch up after a block        # arrives on one upstream but not yet on another        - matchMethod: "eth_call|eth_getLogs"          matchFinality: [realtime, unfinalized]          hedge:            quantile: 0.95            maxCount: 1            minDelay: 500ms            maxDelay: 10s          retry:            maxAttempts: 4            delay: 50ms        - matchMethod: "eth_call|eth_getLogs"          matchFinality: [finalized, unknown]          hedge:            quantile: 0.95            maxCount: 1            minDelay: 500ms            maxDelay: 10s          retry:            maxAttempts: 4            delay: 0        - matchMethod: "*"          retry:            maxAttempts: 6            delay: 50ms    networks:      - architecture: evm        evm: { chainId: 1 }        alias: ethereum      - architecture: evm        evm: { chainId: 42161 }        alias: arbitrum

2. Per-chain bespoke policy appended before the shared catch-all. Ethereum mainnet adds consensus on eth_getLogs and state-read integrity checks; Arbitrum bumps the getLogs hedge floor for its faster blocks. Both spread sharedNetworkFailsafe at the end so the catch-all policy is never per-chain:

projects[].networks[]
erpc.yaml
networks:  - evm: { chainId: 1 }    alias: ethereum    failsafe:      # Ethereum-specific: 2-of-3 consensus on getLogs for indexing integrity      - matchMethod: "eth_getLogs|eth_getBlockReceipts"        matchFinality: [unfinalized, unknown]        timeout: { duration: 15s }        hedge:          quantile: 0.95          maxCount: 1          minDelay: 500ms          maxDelay: 10s        retry:          maxAttempts: 6          delay: 0          emptyResultDelay: 1000ms        # consensus config would follow here      # ... sharedNetworkFailsafe spread after (catch-all always last)
  - evm: { chainId: 42161 }    alias: arbitrum-one    failsafe:      # Arbitrum-specific: getLogs minDelay raised to 1000ms — Arbitrum delivers      # blocks at ~250ms and block-availability races persist slightly longer      - matchMethod: "eth_getLogs|eth_getBlockReceipts"        matchFinality: [unfinalized]        hedge:          quantile: 0.95          maxCount: 1          minDelay: 1000ms          maxDelay: 10s        retry:          maxAttempts: 6          delay: 0      # ... sharedNetworkFailsafe spread after

3. Static genesis-block response for a chain that starts at block 1. Serve a canned reply before the multiplexer and cache so clients don't hit a dead upstream on block 0. The staticResponses array is checked before everything else in the forward pipeline:

projects[].networks[].staticResponses[]
erpc.yaml
networks:  - evm: { chainId: 1328 }    alias: sei-testnet    staticResponses:      - method: eth_getBlockByNumber        params: ["0x0", false]        response:          result:            # Fill the fields your clients read; unrecognised fields are fine            number: "0x0"            hash: "0x0000000000000000000000000000000000000000000000000000000000000000"            parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000"            transactions: []

4. Skip data-integrity validation for non-standard chains. ZkSync Era and other ZK-rollups use a non-standard transactionsRoot (and other encodings) the data-integrity module's recompute checks can't model. The module is opt-in, but if you enabled it project-wide, turn it off for that chain — a network integrity block overrides the project one:

projects[].networks[].integrity
erpc.yaml
networks:  - evm: { chainId: 324 }    alias: zksync-mainnet    # Override the project-wide integrity level to off for this chain    # (or disable just the transactionsRoot checks via integrity.checks).    integrity:      level: off

5. Domain-based aliasing for fully path-free routing. Route an entire subdomain straight to a chain — no project or network segment in the URL path. The alias and domain rule work together so clients call a clean HTTPS endpoint:

server:
  aliasing:
    rules:
      - matchDomain: "eth.rpc.example.com"
        serveProject: main
        serveArchitecture: evm
        serveChain: "1"
      # Wildcard: any unknown subdomain falls through to the main project
      - matchDomain: "*.rpc.example.com"
        serveProject: main
POST https://eth.rpc.example.com/   →  main/evm:1

6. Multiple SVM chains side by side. chain defaults to solana, so a plain Solana cluster keeps the short two-segment id. Naming a fork's chain switches the id to three segments, which is what keeps cache partitions and alias registration from colliding between svm:mainnet-beta and svm:fogo:mainnet. Note that cluster stays per-network — it is identity, so networkDefaults.svm carries only the shared policy:

projects[].networks[]
erpc.yaml
projects:  - id: main    networkDefaults:      svm:        # shared policy only — cluster is network identity and must not be here        commitment: confirmed        statePollerDebounce: 500ms    networks:      # chain omitted → "solana" → networkId "svm:mainnet-beta"      - architecture: svm        svm:          cluster: mainnet-beta      # explicit chain → networkId "svm:fogo:mainnet"      - architecture: svm        svm:          chain: fogo          cluster: mainnet

Both id shapes are addressable the same three ways — URL path, request-body networkId, or an alias. The fork's colon is carried inside the third path segment:

POST /main/svm/mainnet-beta   →  svm:mainnet-beta
POST /main/svm/fogo:mainnet   →  svm:fogo:mainnet
POST /main                    →  body {"networkId": "svm:fogo:mainnet", ...}

Request/response behavior

  • The network label on every Prometheus metric equals the alias when set, otherwise the raw network id (evm:1, svm:mainnet-beta, svm:fogo:mainnet). This affects all erpc_network_* metrics. erpc/networks.go:L278-286 (opens in a new tab)
  • Static-response hits short-circuit before the multiplexer; static_response.hit=true is set on the Network.Forward OTel span. They never read or write the cache.
  • Multiplexer followers receive a copy of the leader's response; if the follower's context deadline fires before the leader completes, the error is ErrNetworkRequestTimeout (HTTP 504 status code, JSON-RPC error code −32603). This is distinct from ErrFailsafeTimeoutExceeded (failsafe policy timeout) and ErrEndpointRequestTimeout (individual upstream HTTP timeout).
  • ErrNetworkInitializing (HTTP 503) is returned while the bootstrap task is still running. The client should retry. Failed (non-fatal) bootstrap tasks are auto-retried by the background loop with factor-1.5 backoff (3 s to 130 s, task timeout 120 s).
  • ErrNetworkNotSupported (HTTP 404) is returned when all providers report the chain is not supported and zero upstreams were registered. It is never retried by the bootstrap loop.
  • ErrNetworkNotFound (HTTP 404) is returned after a successful bootstrap task that stored nothing (defensive path). The caller must retry.
  • ErrInvalidRequest (HTTP 400) is returned when the networkId in the URL is not a valid evm:<int> format — the error message suggests using an alias or evm/42161 form. erpc/networks_registry.go:L220-222 (opens in a new tab)
  • ErrNoUpstreamsFound (HTTP 404) fires when the policy engine returns an empty ordered list and the raw registration list is also empty — all upstreams have been filtered out.
  • ErrUpstreamsExhausted fires when every upstream in the ordered list was tried and all failed; it carries per-upstream errors. erpc/networks.go:L1392-1406 (opens in a new tab)
  • ErrNotImplemented (HTTP 501) is returned for eth_accounts and eth_sign (always); stateful methods with more than one candidate upstream also produce this error unless scoped by a use-upstream directive.
  • ErrInvalidEvmChainId (HTTP 400) is returned when the chain id cannot be parsed.
  • ErrNetworkRequestTimeout wire behavior: JSON-RPC error code −32603; HTTP transport status is 200 (JSON-RPC over HTTP); ErrorStatusCode() returns 504 but this is only seen in non-JSON-RPC error paths. The error is retryable toward both network and upstream (no explicit non-retryable flag). Message format: "network-level request towards one or more upstreams timed out after <N>ms" where N is the elapsed time since the follower registered with the multiplexer.
  • An alias in the request body networkId field is silently ignored — the body fallback splits the literal value on : with no alias lookup. Only URL path segments resolve aliases.

Best practices

  • Always set networkDefaults.failsafe before adding per-chain failsafe entries — once a network has any failsafe list, the defaults list is superseded entirely for that network.
  • Use alias on every statically-declared chain you control. It makes URLs readable, shows up in every Prometheus label, and costs nothing.
  • Set preserveDefaultMethods: true before adding any custom methods.definitions entry. The default false silently drops all built-in methods (including eth_call, eth_getLogs) the moment you add one definition.
  • Never set finalized: true on a method that returns block-specific data (eth_getBalance, eth_getCode, etc.) — every response will be permanently cached at the first value ever returned.
  • Enable servedTip.enabledFor: [latest, finalized] in production multi-pod deployments to prevent backward tip jumps across pods that could surface stale reads to clients.
  • Keep getLogsMaxAllowedRange at its default 30 000 unless your upstreams document a higher limit — range splits happen automatically on oversized requests but getLogsSplitConcurrency caps parallelism at 10 to avoid flooding providers.
  • For eth_sendRawTransaction in high-retry configs, leave idempotentTransactionBroadcast enabled (default). Disabling it turns idempotent "already known" errors back into real failures, making retries unsafe.

Edge cases & gotchas

  1. Aliases work in URLs only, not body networkId — body fallback splits on : with no alias lookup; only parseUrlPath resolves aliases. erpc/http_server.go:L613-634 (opens in a new tab)
  2. Unknown alias segment falls through silently — an unresolvable single path segment is treated as architecture, yielding "architecture is not valid (must be 'evm')" instead of an alias-not-found error.
  3. directiveDefaults is all-or-nothing — a network-level directiveDefaults block completely replaces networkDefaults.directiveDefaults; individual fields are not merged. common/defaults.go:L1837-1840 (opens in a new tab)
  4. failsafe replaces, not merges — any networks[].failsafe list supersedes networkDefaults.failsafe entirely for that network.
  5. preserveDefaultMethods: false + one custom definitions entry drops ALL built-ins — including eth_call, eth_getLogs, etc. Only the custom entries plus the 6 stateful markers survive. common/defaults.go:L561-573 (opens in a new tab)
  6. finalized: true on a non-finalized method permanently caches all responses — every response is placed in the finalized cache bucket (long/permanent TTL). Setting this on eth_getBalance would permanently cache every balance at the first value returned. erpc/networks.go:L1656-1660 (opens in a new tab)
  7. networkDefaults.multiplexing has no deep merge — a network must set its own multiplexing field; there is no per-network override via defaults alone.
  8. Legacy single-object networkDefaults.failsafe silently drops multiplexing — the old-format fallback struct has no Multiplexing field. common/config.go:L626-655 (opens in a new tab)
  9. Lazily-exposed networks never get a metrics alias resolver entryerpc/init.go builds the map only from static cfg.Projects[].Networks[]; gRPC-cache metrics for lazy networks show the raw networkId. erpc/init.go:L62-77 (opens in a new tab)
  10. evm.enforceBlockAvailability: false overrides upstream bounds — step 3 of the enforcement chain (upstream-has-bounds) is only reached when steps 1–2 yield no decision; an explicit false at the network level (step 2) short-circuits it. erpc/networks.go:L1814-1817 (opens in a new tab)
  11. Static response error code 0 is omitted from the wire responseCode is tagged json:"code,omitempty" on int, so 0 disappears from the JSON error object. Always use a non-zero code in production. common/errors.go:L2226 (opens in a new tab)
  12. Failsafe merge picks the FIRST matching default — ordering of networkDefaults.failsafe entries matters when multiple wildcard entries could match.
  13. stateful: false on a built-in stateful method is silently overriddenMethodsConfig.SetDefaults force-sets Stateful = true for all 6 stateful methods in all three code paths, overwriting any user-supplied false without any warning. common/defaults.go:L512-573 (opens in a new tab)
  14. EnforceGetLogsBlockRange and EnforceHighestBlock directives cannot be overridden per-request via HTTP headers for eth_getLogs, trace_filter, arbtrace_filter, and eth_blockNumber — these hooks read from the config baked in at startup, not live per-request directives. Set enforceGetLogsBlockRange: false in directiveDefaults, not as an HTTP header. architecture/evm/eth_getLogs.go:L282-285 (opens in a new tab)
  15. ErrNetworkRequestTimeout is produced only in the multiplexer follower path — a request that is itself the leader experiences ErrFailsafeTimeoutExceeded or upstream sweep errors, never ErrNetworkRequestTimeout. erpc/networks.go:L2079-2084 (opens in a new tab)
  16. evm.chainId: 0 is not rejectedchainId: 0 produces networkId = evm:0, which passes util.IsValidNetworkId but is excluded by provider onlyNetworks/ignoreNetworks lists that use the stricter chainId > 0 check.
  17. translateLatestTag: false on a non-historical method collapses all callers to one cache key — all requests for that method share the literal "latest" key; stale data is returned until the cache TTL expires.
  18. preserveDefaultMethods: false with NO definitions block keeps ALL built-ins — the drop-all behavior only triggers when definitions is non-empty AND preserveDefaultMethods is false. A config with methods: { preserveDefaultMethods: false } and no definitions key is identical to omitting the methods block entirely. common/defaults.go:L494 (opens in a new tab)
  19. Lazy network alias registration happens after first bootstrap — statically-declared networks register aliases eagerly in the NetworksRegistry constructor; lazily-created networks only register their alias inside prepareNetwork, which runs during bootstrap. Until bootstrap completes, alias-based URLs for lazy networks return an alias-not-found error. erpc/networks_registry.go:L322-328 (opens in a new tab)
  20. Failed lazy bootstrap is user-retryable — non-fatal bootstrap tasks are retried by the background auto-retry loop and by the next request's ExecuteTasks. Only resolveNetworkConfig format errors are TaskFatal and never retried. erpc/networks_registry.go:L263-266 (opens in a new tab)
  21. Provider onlyNetworks bypasses vendor support checks entirely — a networkId listed in onlyNetworks returns SupportsNetwork = true without consulting the vendor's dynamic check; ignoreNetworks is evaluated first and short-circuits to false. thirdparty/provider.go:L34-49 (opens in a new tab)
  22. Synthesized lazy network configs become permanently visibleExposeNetworkConfig appends to project.Config.Networks (admin API surface) and never overwrites an existing entry, so lazy networks persist in the admin response after first use. erpc/projects.go:L58-77 (opens in a new tab)
  23. Syncing upstreams are excluded from every head computation — max mode, cluster mode, lowest-finalized, and guaranteed-method floor calculations all skip upstreams in EvmSyncingStateSyncing. erpc/networks.go:L327-330 (opens in a new tab)
  24. Future-block short-circuit compares against the MAX eligible head, not the cluster-min served tip — it never nulls a block that the most-ahead upstream reports having. The synthetic null response is never cached, and the short-circuit only fires for concrete block numbers (not tags or hashes). erpc/networks.go:L570-610 (opens in a new tab)
  25. Wrong-empty misbehavior is suppressed for out-of-bounds blocks — if the block number in a missing-data response falls outside an upstream's configured blockAvailability bounds, the empty response is expected and no misbehavior is recorded against that upstream. erpc/networks.go:L1566-1587 (opens in a new tab)
  26. getLogsMaxAllowedRange: 0 is rejected by validation — the field must be > 0 after defaults (common/validation.go:L1309-1311). Unlike getLogsMaxAllowedAddresses and getLogsMaxAllowedTopics (which default to 0 = unlimited), getLogsMaxAllowedRange defaults to 30 000 and cannot be explicitly zeroed out; setting 0 in a network inheriting from defaults is treated as "unset" and inherits the parent value. common/defaults.go:L2092-2094 (opens in a new tab)
  27. Zero projects → implicit main project + catch-all aliasing rule — when the config has no projects, eRPC auto-creates {matchDomain: "*", serveProject: "main"}, so /evm/123 works without a project path segment. common/defaults.go:L100-110 (opens in a new tab)
  28. Selector-scoped tips never pollute network gauges — stateless scoped picks (unmatched or non-simple selectors) use a sentinel lane and emit no Prometheus gauge; equivalent selectors dedup into one partition keyed by matched-set hash; the cap of 16 partitions is enforced globally per network. erpc/networks.go:L98-105 (opens in a new tab)
  29. Static response params matching has no wildcard — there is no glob or * support; every params slot must match exactly. To catch a method regardless of params, add an entry with an empty params array. To match block 0 ("0x0"), be exact — "0x00" will not match. common/static_response.go:L14-99 (opens in a new tab)

Observability

MetricTypeLabelsWhen it fires
erpc_network_request_received_totalcounterproject, network, category, finality, user, agent_nameOn project forward entry
erpc_network_successful_request_totalcounterproject, network, vendor, upstream, category, attempt, finality, emptyish, user, agent_nameSuccessful response (vendor/upstream = <cache> for cache hits)
erpc_network_failed_request_totalcounterproject, network, category, attempt, error, severity, finality, user, agent_nameFailed response
erpc_network_request_duration_secondshistogramproject, network, vendor, upstream, category, finality, userRequest completed (vendor/upstream = <error> on failure)
erpc_network_multiplexed_request_totalcounterproject, network, category, finality, user, agent_nameFollower registered with the in-flight deduplicator
erpc_network_static_response_served_totalcounterproject, network, categoryStatic response matched and served
erpc_network_timeout_fired_totalcounterproject, network, category, finality, scopeFailsafe timeout fired; scope=network at network level
erpc_network_served_tip_block_numbergaugeproject, network, lane, axisServed-tip value updated; absent in max mode
erpc_network_served_tip_lag_blocksgaugeproject, network, lane, axisMaxEligible - served, clamped ≥ 0; absent in max mode
erpc_network_served_tip_upstream_excluded_totalcounterproject, network, upstream, axis, reasonUpstream excluded from tip computation (velocity|outlier)
erpc_network_hedged_request_totalcounterproject, network, upstream, category, attempt, finality, user, agent_nameHedge attempt dispatched for this upstream
erpc_network_hedge_discards_totalcounterproject, network, upstream, category, attempt, hedge, finality, user, agent_nameHedge response discarded (a faster response already won)
erpc_upstream_wrong_empty_response_totalcounterproject, vendor, network, upstream, category, finality, user, agent_nameUpstream returned missing-data while another upstream served real data; suppressed when the block is outside the upstream's blockAvailability bounds
erpc_unexpected_panic_totalcounter(standard labels)Panic recovered in the async cache-set goroutine

Notable logs (zerolog, at erpc/networks_registry.go and erpc/networks.go unless noted):

  • "registered network alias" / "skipping duplicate alias registration with different target" — alias map events.
  • "network initialization ended with zero upstreams" — bootstrap completed but no upstreams were registered.
  • "networks bootstrap completed" / "failed to bootstrap networks in background" — startup background bootstrap outcome.
  • "response served from cache" — cache hit in the forward pipeline.
  • "found identical request initiating multiplexer" — multiplexer follower registered.
  • "block availability check failed; failing open to allow request" — block-availability extraction or poller error; request proceeds.
  • "served static response (no upstream contacted)" (debug) — static response matched and served (erpc/networks_static_responses.go).
  • "skipping static response: cannot inspect request" (debug) — JsonRpcRequest() failed; static check skipped.
  • "failed to build static response" (error) — NewJsonRpcResponse failed on a matched static entry.
  • Provider task: "registering N upstream(s) from provider" (upstream/registry.go).

Source code entry points

Related pages