Config
Upstreams

Upstreams

AIOpen as plain markdown for AI

An upstream is a single RPC endpoint (yours or a third party's) that can serve one or more EVM networks. eRPC manages the pool: it scores upstreams in real time, picks the best one as primary, falls over to the rest on failure, and enforces per-upstream rate limits and method allow/deny lists.

You can configure:

  • Endpoint — plain HTTPS URL, or a vendor-shorthand like alchemy://API_KEY (see Providers)
  • EVM details — chain ID, state poller cadence, block-availability bounds with probes
  • Method filtersignoreMethods, allowMethods, autoIgnoreUnsupportedMethods
  • Failsafe — per-upstream timeout, retry, circuitBreaker, hedge, consensus, with optional matchMethod / matchFinality scoping
  • Rate limits — bind a rateLimitBudget, optionally with rateLimitAutoTune that adjusts the budget based on 429 feedback
  • Routing & scoring — per-upstream routing.scoreMultipliers to boost/penalize this endpoint relative to others
  • Transport — JSON-RPC batching, gzip, custom headers, outbound proxy pool
  • Groupinggroup, vendorName for labelling and fallback policies
  • Shadow trafficshadow to mirror a fraction of requests to this upstream for comparison without affecting clients
  • Per-method response validationevm.integrity.eth_getBlockReceipts correctness checks

Minimum useful config

The smallest workable upstream is just an endpoint. Everything else has sensible defaults.

projectsupstreams[]
erpc.yaml
projects:  - id: main    upstreams:      - endpoint: https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY        # eRPC auto-detects the chain ID and applies default failsafe (15s timeout,        # 2 retries, circuit breaker at 80% failure rate).

Production config — every common knob

A realistic upstream with rate limits, batching, method filters, and tuned failsafe. The dimmed scaffolding shows you where each block plugs in.

projectsupstreams[]
erpc.yaml
projects:  - id: main    rateLimiters:      budgets:        - id: alchemy-global          rules: [{ method: "*", maxCount: 500, period: second }]    upstreams:      - id: my-alchemy                      # used in logs, metrics, selection-policy        endpoint: https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY        rateLimitBudget: alchemy-global     # binds to budget above        evm:          chainId: 1                        # optional, auto-detected        jsonRpc:          supportsBatch: true          batchMaxSize: 10          batchMaxWait: 50ms          enableGzip: false                 # compress outbound bodies; default off        ignoreMethods: ["debug_*"]          # never send these here        allowMethods: ["eth_*", "net_*"]    # if set, ALL others are blocked except listed        failsafe:          - matchMethod: "*"            timeout: { duration: 15s }            retry:              maxAttempts: 3              delay: 200ms              backoffFactor: 1.5              jitter: 50ms            circuitBreaker:              failureThresholdCount: 160              failureThresholdCapacity: 200              halfOpenAfter: 5m              successThresholdCount: 3              successThresholdCapacity: 10

Priority & routing

eRPC scores each upstream every few seconds based on error rate, latency, throttling, and block lag. The top-scoring upstream is the primary; failures rotate to the runner-up. Defaults work well in production; tune only when you have a reason.

projects[]
erpc.yaml
projects:  - id: main    routingStrategy: score-based   # or "round-robin"    scoreGranularity: upstream     # or "method" for per-method scoring    scoreRefreshInterval: 30s    scoreMetricsWindowSize: 10m    scorePenaltyDecayRate: 0.95    # 0..1; higher = stabler; -1 = no smoothing    scoreSwitchHysteresis: 0.10    # challenger must beat primary by this fraction    scoreMinSwitchInterval: 2m     # cooldown between primary switches    upstreams: [ /* ... */ ]

The scoring mechanism only changes the order in which upstreams are tried — it doesn't disable any. To fully take an upstream offline, use the circuit breaker failsafe or ignoreMethods: ["*"].

Block availability

Bound the block window an upstream can serve. The bound can be relative (latestBlockMinus, earliestBlockPlus), absolute (exactBlock), or probe-driven (auto-detect the earliest block where logs, calls, or traces are available). Block-availability bounds skip out-of-range upstreams before retry rather than failing-then-retrying.

projectsupstreams[]evmblockAvailability
erpc.yaml
projects:  - id: main    upstreams:      - id: my-archive        endpoint: https://archive.example        evm:          blockAvailability:            # Don't serve the last 64 blocks (avoid reorgs)            upper:              latestBlockMinus: 64              probe: blockHeader     # default probe            # Auto-detect earliest block where logs exist, refresh hourly            lower:              earliestBlockPlus: 0              probe: eventLogs              updateRate: 1h

For an upstream that is a point-in-time snapshot (e.g. a frozen archive at a specific block), use exactBlock to pin both bounds to the same height:

upstreams:
  - id: snapshot-19m
    endpoint: https://snapshot.example
    evm:
      blockAvailability:
        upper:
          exactBlock: 19000000   # only serve requests at or below this block
        lower:
          exactBlock: 0          # serve from genesis

exactBlock is a static value — it never updates. Use it when the upstream's range will never change (snapshots, genesis-only nodes). For nodes that advance over time, prefer latestBlockMinus / earliestBlockPlus with a probe.

ProbeWhat it checks
blockHeader (default)eth_getBlockByNumber(blockHash) returns a header.
eventLogseth_getLogs(blockHash) returns ≥1 log. Useful as a lower bound for archive nodes that prune logs.
callStateeth_getBalance returns a non-null result. Probes historical state availability.
traceDataTries trace_block, debug_traceBlockByHash, trace_replayBlockTransactions in order; available if any returns.

Block-availability bounds are only enforced when eRPC can extract a block number from the request. For methods without an explicit block parameter (e.g. eth_chainId), the request goes to the upstream regardless of the bounds. updateRate only applies to earliestBlockPluslatestBlockMinus always reads the live head.

Upstream types

The only type today is evm (default; covers any EVM-compatible JSON-RPC endpoint, whether self-hosted or third-party). Vendor shorthands like alchemy://, drpc://, infura:// are handled by the providers layer on top of type: evm — they're not separate types.

Copy for your AI assistant — full upstreams referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

Every field on upstreams[]

FieldTypeNotes
idstringUsed in logs, metrics, selection-policy eval. Auto-generated if omitted.
typestringevm (default and only supported value today).
endpointstring (required)HTTPS URL or vendor shorthand (alchemy://KEY, drpc://KEY, repository://URL, etc.).
groupstringArbitrary label. Used as a metric label, and as a target in selection-policy eval functions. The literal value "fallback" is special — see "Groups & the fallback magic value" below.
vendorNamestringTag an upstream with a known vendor identifier so vendor-specific normalization (error code mapping, etc.) applies, even when the endpoint is a plain URL. Normally set automatically by provider shorthands (alchemy://, drpc://, etc.). Set it manually on plain-URL upstreams to opt into the same normalization — e.g. a self-hosted Erigon node behind nginx can set vendorName: erigon to enable Erigon-specific error parsing without using a vendor shorthand.
evmobjectEVM-specific config — see "evm.* fields" below.
jsonRpcobjectTransport-level config: batching, gzip, headers, proxy pool. See "jsonRpc.* fields" below.
ignoreMethodsstring[]Block these methods on this upstream (matcher syntax: eth_*, debug_*|trace_*, etc.).
allowMethodsstring[]Allowlist; if set, blocks everything not listed. When allowMethods is set and ignoreMethods is not, ignoreMethods: ["*"] is implicit.
autoIgnoreUnsupportedMethodsboolDefault true. When an upstream returns "method not supported", auto-add the method to ignoreMethods for this upstream.
failsafearrayPer-upstream failsafe policies; see Failsafe docs. Supports matchMethod and matchFinality per entry.
rateLimitBudgetstringBind to a budget defined in rateLimiters.budgets[]. Multiple upstreams sharing a budget share the same rate-limit pool.
rateLimitAutoTuneobjectWhen a budget is bound, auto-tune is enabled by default. See "rateLimitAutoTune fields".
routingobjectPer-upstream score multipliers and latency quantile. See "routing.* fields".
shadowobjectMirror a fraction of traffic to this upstream for comparison (without affecting the real response). See "Shadow upstreams" below.

evm.* fields

FieldDefaultNotes
chainIdauto-detected via eth_chainIdSet explicitly to skip detection at startup.
statePollerInterval30sHow often to poll latest/finalized/syncing. Set to 0s to disable polling entirely — all data will be treated as unfinalized or unknown.
statePollerDebouncedynamicOverride the polling debounce. When omitted, eRPC infers it from the observed block time.
skipWhenSyncingfalseWhen true, route requests away from this upstream while eth_syncing reports it's syncing. Use for archive backfills.
blockAvailabilitynoneSee "Block availability" section above.
integrity.eth_getBlockReceiptsnonePer-upstream response validation for eth_getBlockReceipts. See "Per-upstream integrity" below.
nodeTypearchiveDeprecated — use blockAvailability instead. Still accepted; full implies a 128-block availability window.
maxAvailableRecentBlocks128 (for full nodes)Deprecated — use blockAvailability instead.
getLogsAutoSplittingRangeThresholdnoneUpstream hint for the network-level proactive splitter. The network computes the min positive threshold across selected upstreams and splits large eth_getLogs ranges into contiguous sub-requests of at most that size. Set to 0 or negative to disable for this upstream.
traceFilterAutoSplittingRangeThresholdnoneSame as above for trace_filter / arbtrace_filter.

Deprecated: getLogsMaxAllowedRange, getLogsMaxAllowedAddresses, getLogsMaxAllowedTopics, getLogsSplitOnError on upstream.evm — these moved to the network level.

jsonRpc.* fields

FieldDefaultNotes
supportsBatchfalseAllow eRPC to batch outbound requests to this upstream. Even when false, clients can still send batch requests to eRPC — they'll be unrolled into individual upstream calls.
batchMaxSize10Max requests per outbound batch.
batchMaxWait50msMax time to wait while filling a batch before flushing.
enableGzipfalseCompress outbound request bodies. Most upstreams ignore this; turn on only when the vendor documents support.
headers{}Extra headers on every request — typically Authorization: Bearer ... for private endpoints.
proxyPoolnoneID of a proxy pool from the top-level proxyPools[] — outbound requests round-robin through that pool.

Method filters — interaction rules

  • Both ignoreMethods and allowMethods accept matcher syntax (* wildcard, | OR).
  • allowMethods takes precedence over ignoreMethods when both match.
  • Setting allowMethods without ignoreMethods implicitly adds ignoreMethods: ["*"] — you must explicitly set ignoreMethods: [] to opt out of that behavior.
  • autoIgnoreUnsupportedMethods (default true) augments ignoreMethods at runtime when the upstream returns a "method not supported" error. Disable when probing experimental methods.

Groups & the fallback magic value

group is a free-form label, but the value "fallback" is special:

  • If any upstream in a network has group: fallback, eRPC auto-creates a default network-level selection policy that prefers non-fallback upstreams and only reaches into the fallback group when the primaries are unhealthy.
  • This behavior is enabled by the ROUTING_POLICY_* env-controlled defaults — see selection policies for the exact eval body.
  • Any other group value is purely a label and only matters if you reference it from your own selection-policy evalFunction or want it as a metric label.

Apart from the fallback magic value, you can use group purely as a label that's referenced by your own selection-policy evalFunction and surfaces in Prometheus metric labels.

routing.* — per-upstream score tuning

upstreams:
  - id: my-alchemy
    endpoint: https://eth-mainnet.g.alchemy.com/v2/KEY
    routing:
      scoreLatencyQuantile: 0.70           # latency percentile used for scoring (default 0.70)
      scoreMultipliers:
        # ── Selectors: which (network × method × finality) bucket these weights apply to.
        - network: "*"                     # matcher: "evm:1", "evm:*", "evm:1|evm:10"
          method: "*"                      # matcher: "eth_getLogs|eth_call", etc.
          finality: ["realtime", "unfinalized"]  # filter — apply only to these finality buckets
          # ── Weights (numeric). `overall` is a final multiplier; the others scale individual penalties.
          overall: 1.0                     # boost the final score (>1 = preferred)
          errorRate: 4.0                   # higher = bigger penalty when error rate is bad
          respLatency: 8.0                 # higher = bigger penalty for slow tail latency
          throttledRate: 3.0
          blockHeadLag: 2.0
          finalizationLag: 1.0
          totalRequests: 1.0               # weight on observed request volume (favor warmer pools)
          misbehaviors: 5.0                # weight on consensus-misbehavior count

How scoring works. Each upstream is scored separately for every (network × method × finality) bucket it serves. The score is a weighted aggregate of penalty signals (error rate, latency quantile, throttling, head-lag, finalization-lag, misbehavior count, request volume); higher weight on a penalty dimension means that metric hurts the score more. overall is the final multiplier applied on top, so an upstream with overall: 10 and slightly worse metrics still outranks one with overall: 1 and a perfect score.

Selectors vs weights:

  • network, method, finality are selectors — they decide which buckets this entry applies to. finality is an array (["realtime", "unfinalized"]); valid values are finalized, unfinalized, realtime, unknown. Omitting finality matches all buckets.
  • overall, errorRate, respLatency, throttledRate, blockHeadLag, finalizationLag, totalRequests, misbehaviors are weights (floats, default 1.0). Setting one to 0 removes that signal from the score for the selected buckets.
  • Default weights: if no scoreMultipliers entry matches a given bucket (because no entry exists, or no selector matches), every dimension defaults to 1.0 and overall defaults to 1.0 — uniform weight across all signals, no boost.

Worked example. Suppose upstream A has errorRate: 8.0 and upstream B has errorRate: 1.0. Both observe a 5% error rate in the same window. The errorRate penalty contribution for A is 0.05 × 8.0 = 0.40; for B it is 0.05 × 1.0 = 0.05. That 0.35-point gap compounds with the other dimensions — A scores meaningfully lower and is deprioritized sooner and more aggressively than B, even though both see identical traffic. Setting errorRate: 8.0 on a latency-sensitive bucket is a way of saying "I care much more about errors than the default here."

Multipliers compose: an upstream with overall: 10 and a perfect score will outrank one with overall: 1 even at slightly worse metrics. To prefer a cheap upstream by default:

upstreams:
  - { id: cheap, endpoint: ..., routing: { scoreMultipliers: [{ overall: 10 }] } }
  - { id: pricey, endpoint: ..., routing: { scoreMultipliers: [{ overall: 1 }] } }

Tuning tips:

  • Fast failover — set scoreSwitchHysteresis: -1 and scoreMinSwitchInterval: -1 to always use the current best upstream immediately.
  • Smoother scoring — increase scorePenaltyDecayRate toward 0.98 so transient blips don't move the primary.
  • Reactive scoring — decrease toward 0.80 so degradation kicks in quickly.

When to tune what:

  • High respLatency weight — use when tail-latency variance between providers is large and client timeout budget is tight. Raising respLatency to 8.016.0 makes slow p70 latency a dominant signal.
  • High misbehaviors weight — use when running consensus failsafe and you want divergent upstreams de-prioritized aggressively after they produce wrong answers. Default 1.0 is gentle; 10.0+ will effectively exile a misbehaving upstream until its penalty decays.
  • Narrow method-scoped entry — use when an upstream is excellent for some methods but bad for others. A separate entry with a tight method selector applies heavier penalty only where the upstream is weak:
routing:
  scoreMultipliers:
    - method: '*'
      overall: 2.0         # generally preferred
    - method: 'eth_getLogs'
      respLatency: 16.0    # but penalize hard for getLogs where it's slow
      errorRate: 4.0

rateLimitAutoTune fields

rateLimitAutoTune:
  enabled: true              # default: true if a rateLimitBudget is bound
  adjustmentPeriod: 1m       # window for evaluating throttled ratio
  errorRateThreshold: 0.1    # if throttled-rate > this, decrease
  increaseFactor: 1.05       # multiply budget by this when below threshold
  decreaseFactor: 0.9        # multiply budget by this when above threshold
  minBudget: 0
  maxBudget: 10000

The new budget applies to every upstream sharing that budget — auto-tune decreases on one Quicknode upstream tighten the budget for all Quicknode upstreams sharing it.

Per-upstream integrity — evm.integrity

Validate eth_getBlockReceipts responses on this upstream specifically. Useful when a vendor has known correctness issues you want to catch before the response is cached or returned.

upstreams:
  - id: suspect-vendor
    endpoint: https://...
    evm:
      integrity:
        eth_getBlockReceipts:
          enabled: true
          checkLogIndexStrictIncrements: true   # log.index must be strictly increasing within a block
          checkLogsBloom: true                   # recalculated bloom must match the header's logsBloom

When a check fails, the response is treated as an upstream error — retried, scored against, and optionally exported via consensus.misbehaviorsDestination.

Shadow upstreams

shadow mirrors a fraction of real traffic to this upstream without affecting the response sent to the client. The shadow result is compared against the primary's; mismatches are logged or exported. Useful for vendor evaluations, regression detection, and silent validation of new endpoints.

upstreams:
  # Primary serves traffic normally.
  - id: primary
    endpoint: https://prod-vendor.example
  # Shadow only — receives a 10% sample of every request the primary serves.
  - id: candidate
    endpoint: https://candidate-vendor.example
    shadow:
      enabled: true
      sampleRate: 0.1                  # 0.0–1.0; fraction of requests to mirror
      ignoreFields:                    # diff-comparison ignore lists
        "*": ["blockTimestamp"]
        "transactions.*": ["gasPrice"]

Notes:

  • Shadow upstreams don't count in selection — they only see traffic that the primary served first.
  • Diffs are emitted as a metric (erpc_upstream_shadow_diff_total) and, when paired with a misbehaviors destination, written out for inspection.
  • ignoreFields uses the same dot-path matcher as consensus.ignoreFields* matches a single segment, ** matches any depth.

Failsafe at upstream level (matchMethod + matchFinality)

failsafe[] accepts per-policy matchMethod and matchFinality so you can have different retry budgets for different categories of methods on the same upstream:

upstreams:
  - id: archive
    endpoint: https://archive.example
    failsafe:
      - matchMethod: "trace_*|debug_*"
        timeout: { duration: 60s }
        retry: { maxAttempts: 1 }            # expensive — don't multiply
      - matchMethod: "*"
        matchFinality: ["realtime", "unfinalized"]
        timeout: { duration: 5s }
        retry: { maxAttempts: 3, delay: 100ms }
      - matchMethod: "*"
        matchFinality: ["finalized"]
        timeout: { duration: 30s }
        retry: { maxAttempts: 5, delay: 200ms }

consensus and hedge are also valid at upstream level. The full vocabulary is the same as the network-level failsafe.

The legacy single-object form (failsafe: { timeout: ... }) is still accepted for backward compatibility, but the array form with matchMethod: "*" is the canonical shape and lets you grow into per-method tuning without rewriting.

Defaults via upstreamDefaults

project.upstreamDefaults applies before any per-upstream config — useful for proxy pools, gzip, or a baseline failsafe across every upstream in a project.

projects:
  - id: main
    upstreamDefaults:
      jsonRpc:
        proxyPool: eu-dc1-pool
        enableGzip: true
      failsafe:
        - matchMethod: "*"
          timeout: { duration: 20s }
          retry: { maxAttempts: 2 }
    upstreams:
      - id: a
        endpoint: https://a.example          # inherits the defaults above
      - id: b
        endpoint: https://b.example
        jsonRpc:
          proxyPool: us-dc1-pool             # per-upstream override

Per-upstream values override the defaults; arrays don't merge.

Outbound compression

eRPC supports gzip at four points:

DirectionControlDefault
Client → eRPCClient sets Content-Encoding: gzip.Always accepted.
eRPC → Upstreamupstreams[].jsonRpc.enableGzipfalse
Upstream → eRPCAutomatic if upstream sends Content-Encoding: gzip.Always accepted.
eRPC → Clientserver.enableGziptrue
# Example client → eRPC gzipped request
curl -X POST \
  -H "Content-Encoding: gzip" \
  -H "Content-Type: application/json" \
  --data-binary @<(echo '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[]}' | gzip) \
  http://localhost:4000/main/evm/42161

Custom HTTP headers

upstreams:
  - id: private
    endpoint: https://private-provider.io/v1
    jsonRpc:
      headers:
        Authorization: Bearer SECRET_VALUE
        X-Custom-Header: HelloWorld

Headers are applied on every outbound request from eRPC to this upstream.

Proxy pools

A proxy pool is a named list of outbound HTTP/SOCKS proxies. When an upstream (or upstreamDefaults) references a pool by ID via jsonRpc.proxyPool, every request eRPC makes to that upstream goes through one of the pool's proxies. eRPC round-robins across proxies using an atomic counter — each successive request picks the next proxy in sequence. Useful for geographic distribution, egress IP pinning, ISP routing, or private/public splits.

proxyPools is defined at the root of the config, not inside a project. All projects share the same pool list.

proxyPools[]
erpc.yaml
proxyPools:  - id: eu-dc1-pool    urls:      - http://proxy111.myorg.local:3128      - https://proxy222.myorg.local:3129  - id: us-dc1-pool    urls:      - http://proxy333.myorg.local:3128      - socks5://user:pass@proxy444.myorg.local:1080
projects:  - id: main    upstreamDefaults:      jsonRpc:        proxyPool: eu-dc1-pool        # default for all upstreams in this project    upstreams:      - id: us-rpc        endpoint: https://us.example        jsonRpc:          proxyPool: us-dc1-pool      # override for this upstream      - id: direct-rpc        endpoint: https://direct.example        jsonRpc:          proxyPool: ''               # opt out (empty string disables the inherited default)

proxyPools[] fields

FieldTypeNotes
idstring (required)Identifier referenced by jsonRpc.proxyPool on upstreams or upstreamDefaults.
urlsstring[] (required)One or more proxy URLs. At least one is required. eRPC round-robins across them per request.

Accepted URL schemes

SchemeNotes
http://Plain HTTP CONNECT proxy.
https://TLS-wrapped HTTP CONNECT proxy.
socks5://SOCKS5 proxy. Credentials embedded in the URL: socks5://user:pass@host:port.

Round-robin and failover

eRPC selects proxies with a lockless atomic counter — each request increments the counter and picks counter % len(urls). There is no automatic failover: if the selected proxy is unreachable, the upstream request fails and normal upstream-level retry/circuit-breaker logic applies. To tolerate proxy failures, put a single reliable entry per pool or front your proxies with a load balancer.

Deprecated fields — migration map

Old (still accepted)New
evm.nodeType: full / archiveevm.blockAvailability.upper.latestBlockMinus: 128 (or whatever your window is)
evm.maxAvailableRecentBlocks: 128evm.blockAvailability.upper.latestBlockMinus: 128
evm.getLogsMaxAllowedRange (upstream level)network.evm.getLogsMaxAllowedRange
evm.getLogsMaxAllowedAddresses (upstream level)network.evm.getLogsMaxAllowedAddresses
evm.getLogsMaxAllowedTopics (upstream level)network.evm.getLogsMaxAllowedTopics
evm.getLogsSplitOnError (upstream level)network.evm.getLogsSplitOnError
evm.getLogsMaxBlockRange (upstream level)network.evm.getLogsMaxAllowedRange
failsafe: { ... } (single object)failsafe: [{ matchMethod: "*", ... }]

The legacy forms still parse, so existing configs work — but new code should use the new shape. Mixing both in one config will emit a deprecation warning.

Common pitfalls

  • allowMethods without ignoreMethods: []allowMethods silently adds an implicit ignoreMethods: ["*"], so an upstream with allowMethods: ["eth_getLogs"] will block every other method. To allow eth_getLogs while still serving the defaults, prefer adding to ignoreMethods instead.
  • group: fallback is magic — using this value triggers auto-creation of a default selection policy. Use any other label if you don't want that behavior.
  • Compound rate-limit budgetsrateLimitAutoTune decreases on one upstream tighten the budget for every upstream sharing it. If you need independent rate limits per upstream, give each its own budget.
  • statePollerInterval: 0s — disables polling entirely, so eRPC has no notion of the chain's latest/finalized state for this upstream. Every response goes into the unknown finality bucket; the cache layer will treat them accordingly.
  • blockAvailability.upper.updateRate is ignored — for latestBlockMinus the bound is computed on-demand from the state poller's latest head. Only earliestBlockPlus honors updateRate.

Append .llms.txt to this URL (or use the AI link above) to fetch the entire expanded reference as plain markdown for an AI assistant.