Upstreams
AIOpen as plain markdown for AIAn 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 filters —
ignoreMethods,allowMethods,autoIgnoreUnsupportedMethods - Failsafe — per-upstream
timeout,retry,circuitBreaker,hedge,consensus, with optionalmatchMethod/matchFinalityscoping - Rate limits — bind a
rateLimitBudget, optionally withrateLimitAutoTunethat adjusts the budget based on 429 feedback - Routing & scoring — per-upstream
routing.scoreMultipliersto boost/penalize this endpoint relative to others - Transport — JSON-RPC batching, gzip, custom headers, outbound proxy pool
- Grouping —
group,vendorNamefor labelling and fallback policies - Shadow traffic —
shadowto mirror a fraction of requests to this upstream for comparison without affecting clients - Per-method response validation —
evm.integrity.eth_getBlockReceiptscorrectness checks
Minimum useful config
The smallest workable upstream is just an endpoint. Everything else has sensible defaults.
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.
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: 10Priority & 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: - 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.
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: 1hFor 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 genesisexactBlock 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.
| Probe | What it checks |
|---|---|
blockHeader (default) | eth_getBlockByNumber(blockHash) returns a header. |
eventLogs | eth_getLogs(blockHash) returns ≥1 log. Useful as a lower bound for archive nodes that prune logs. |
callState | eth_getBalance returns a non-null result. Probes historical state availability. |
traceData | Tries 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 earliestBlockPlus — latestBlockMinus 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[]
| Field | Type | Notes |
|---|---|---|
id | string | Used in logs, metrics, selection-policy eval. Auto-generated if omitted. |
type | string | evm (default and only supported value today). |
endpoint | string (required) | HTTPS URL or vendor shorthand (alchemy://KEY, drpc://KEY, repository://URL, etc.). |
group | string | Arbitrary 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. |
vendorName | string | Tag 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. |
evm | object | EVM-specific config — see "evm.* fields" below. |
jsonRpc | object | Transport-level config: batching, gzip, headers, proxy pool. See "jsonRpc.* fields" below. |
ignoreMethods | string[] | Block these methods on this upstream (matcher syntax: eth_*, debug_*|trace_*, etc.). |
allowMethods | string[] | Allowlist; if set, blocks everything not listed. When allowMethods is set and ignoreMethods is not, ignoreMethods: ["*"] is implicit. |
autoIgnoreUnsupportedMethods | bool | Default true. When an upstream returns "method not supported", auto-add the method to ignoreMethods for this upstream. |
failsafe | array | Per-upstream failsafe policies; see Failsafe docs. Supports matchMethod and matchFinality per entry. |
rateLimitBudget | string | Bind to a budget defined in rateLimiters.budgets[]. Multiple upstreams sharing a budget share the same rate-limit pool. |
rateLimitAutoTune | object | When a budget is bound, auto-tune is enabled by default. See "rateLimitAutoTune fields". |
routing | object | Per-upstream score multipliers and latency quantile. See "routing.* fields". |
shadow | object | Mirror a fraction of traffic to this upstream for comparison (without affecting the real response). See "Shadow upstreams" below. |
evm.* fields
| Field | Default | Notes |
|---|---|---|
chainId | auto-detected via eth_chainId | Set explicitly to skip detection at startup. |
statePollerInterval | 30s | How often to poll latest/finalized/syncing. Set to 0s to disable polling entirely — all data will be treated as unfinalized or unknown. |
statePollerDebounce | dynamic | Override the polling debounce. When omitted, eRPC infers it from the observed block time. |
skipWhenSyncing | false | When true, route requests away from this upstream while eth_syncing reports it's syncing. Use for archive backfills. |
blockAvailability | none | See "Block availability" section above. |
integrity.eth_getBlockReceipts | none | Per-upstream response validation for eth_getBlockReceipts. See "Per-upstream integrity" below. |
nodeType | archive | Deprecated — use blockAvailability instead. Still accepted; full implies a 128-block availability window. |
maxAvailableRecentBlocks | 128 (for full nodes) | Deprecated — use blockAvailability instead. |
getLogsAutoSplittingRangeThreshold | none | Upstream 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. |
traceFilterAutoSplittingRangeThreshold | none | Same as above for trace_filter / arbtrace_filter. |
Deprecated: getLogsMaxAllowedRange, getLogsMaxAllowedAddresses, getLogsMaxAllowedTopics, getLogsSplitOnError on upstream.evm — these moved to the network level.
jsonRpc.* fields
| Field | Default | Notes |
|---|---|---|
supportsBatch | false | Allow 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. |
batchMaxSize | 10 | Max requests per outbound batch. |
batchMaxWait | 50ms | Max time to wait while filling a batch before flushing. |
enableGzip | false | Compress 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. |
proxyPool | none | ID of a proxy pool from the top-level proxyPools[] — outbound requests round-robin through that pool. |
Method filters — interaction rules
- Both
ignoreMethodsandallowMethodsaccept matcher syntax (*wildcard,|OR). allowMethodstakes precedence overignoreMethodswhen both match.- Setting
allowMethodswithoutignoreMethodsimplicitly addsignoreMethods: ["*"]— you must explicitly setignoreMethods: []to opt out of that behavior. autoIgnoreUnsupportedMethods(defaulttrue) augmentsignoreMethodsat 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
groupvalue is purely a label and only matters if you reference it from your own selection-policyevalFunctionor 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 countHow 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,finalityare selectors — they decide which buckets this entry applies to.finalityis an array (["realtime", "unfinalized"]); valid values arefinalized,unfinalized,realtime,unknown. Omittingfinalitymatches all buckets.overall,errorRate,respLatency,throttledRate,blockHeadLag,finalizationLag,totalRequests,misbehaviorsare weights (floats, default1.0). Setting one to0removes that signal from the score for the selected buckets.- Default weights: if no
scoreMultipliersentry matches a given bucket (because no entry exists, or no selector matches), every dimension defaults to1.0andoveralldefaults to1.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: -1andscoreMinSwitchInterval: -1to always use the current best upstream immediately. - Smoother scoring — increase
scorePenaltyDecayRatetoward0.98so transient blips don't move the primary. - Reactive scoring — decrease toward
0.80so degradation kicks in quickly.
When to tune what:
- High
respLatencyweight — use when tail-latency variance between providers is large and client timeout budget is tight. RaisingrespLatencyto8.0–16.0makes slow p70 latency a dominant signal. - High
misbehaviorsweight — use when runningconsensusfailsafe and you want divergent upstreams de-prioritized aggressively after they produce wrong answers. Default1.0is 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
methodselector 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.0rateLimitAutoTune 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: 10000The 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 logsBloomWhen 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. ignoreFieldsuses the same dot-path matcher asconsensus.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 overridePer-upstream values override the defaults; arrays don't merge.
Outbound compression
eRPC supports gzip at four points:
| Direction | Control | Default |
|---|---|---|
| Client → eRPC | Client sets Content-Encoding: gzip. | Always accepted. |
| eRPC → Upstream | upstreams[].jsonRpc.enableGzip | false |
| Upstream → eRPC | Automatic if upstream sends Content-Encoding: gzip. | Always accepted. |
| eRPC → Client | server.enableGzip | true |
# 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/42161Custom HTTP headers
upstreams:
- id: private
endpoint: https://private-provider.io/v1
jsonRpc:
headers:
Authorization: Bearer SECRET_VALUE
X-Custom-Header: HelloWorldHeaders 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: - 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
| Field | Type | Notes |
|---|---|---|
id | string (required) | Identifier referenced by jsonRpc.proxyPool on upstreams or upstreamDefaults. |
urls | string[] (required) | One or more proxy URLs. At least one is required. eRPC round-robins across them per request. |
Accepted URL schemes
| Scheme | Notes |
|---|---|
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 / archive | evm.blockAvailability.upper.latestBlockMinus: 128 (or whatever your window is) |
evm.maxAvailableRecentBlocks: 128 | evm.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
allowMethodswithoutignoreMethods: []—allowMethodssilently adds an implicitignoreMethods: ["*"], so an upstream withallowMethods: ["eth_getLogs"]will block every other method. To alloweth_getLogswhile still serving the defaults, prefer adding toignoreMethodsinstead.group: fallbackis 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 budgets —
rateLimitAutoTunedecreases 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 theunknownfinality bucket; the cache layer will treat them accordingly.blockAvailability.upper.updateRateis ignored — forlatestBlockMinusthe bound is computed on-demand from the state poller's latest head. OnlyearliestBlockPlushonorsupdateRate.
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.