Selection Policies
AIOpen as plain markdown for AIA selection policy is a JS eval function that runs on a periodic interval and returns the subset of upstreams that are eligible to serve a network (or a specific method). Think of it as a recurring healthcheck that gates routing — unhealthy or lagging upstreams are excluded until they recover.
You can configure:
evalInterval— how often to re-evaluate eligibility (e.g.1m,30s)evalFunction— arbitrary JS that receives all upstreams and returns the ones to includeevalPerMethod— run the eval separately for each RPC method instead of once per networkresampleExcluded— periodically let excluded upstreams handle a few sample requests so their metrics refresh (circuit-breaker "half-open" pattern)resampleInterval/resampleCount— cadence and volume of resampling
Minimal config
projects: - id: main networks: - architecture: evm evm: chainId: 1 selectionPolicy: evalInterval: 1m evalFunction: | (upstreams, method) => { const healthy = upstreams.filter( u => u.metrics.errorRate < 0.7 && u.metrics.blockHeadLag < 10 ); return healthy.length > 0 ? healthy : upstreams; }Selection policies run on an interval — not per request. They update the set of eligible upstreams in the background. Within the eval interval, the previous decision stays in effect.
Default fallback policy
If any upstream has group: "fallback", eRPC automatically activates a built-in selection policy. Non-fallback upstreams are used by default; if too few are healthy, the fallback group is included. The thresholds are tunable via environment variables without rewriting the eval function:
ROUTING_POLICY_MAX_ERROR_RATE(default0.7) — maximum allowed error rateROUTING_POLICY_MAX_BLOCK_HEAD_LAG(default10) — maximum blocks behind the network's highest known head (block-number delta, not seconds —10is ~120s on Ethereum, ~2.5s on Arbitrum)ROUTING_POLICY_MIN_HEALTHY_THRESHOLD(default1) — minimum healthy non-fallback upstreams before the fallback group is included
Fallback group example
projects: - id: main upstreams: - endpoint: cheap-1.com - endpoint: cheap-2.com - endpoint: fast-1.com group: fallback - endpoint: fast-2.com group: fallback networks: - architecture: evm evm: chainId: 1 selectionPolicy: evalInterval: 1m evalFunction: | (upstreams, method) => { const defaults = upstreams.filter(u => u.config.group !== 'fallback'); const fallbacks = upstreams.filter(u => u.config.group === 'fallback'); const maxErrorRate = parseFloat(process.env.ROUTING_POLICY_MAX_ERROR_RATE || '0.7'); const maxBlockHeadLag = parseFloat(process.env.ROUTING_POLICY_MAX_BLOCK_HEAD_LAG || '10'); const minHealthy = parseInt(process.env.ROUTING_POLICY_MIN_HEALTHY_THRESHOLD || '1'); const healthyOnes = defaults.filter( u => u.metrics.errorRate < maxErrorRate && u.metrics.blockHeadLag < maxBlockHeadLag ); if (healthyOnes.length >= minHealthy) { return healthyOnes; } return upstreams; } resampleExcluded: true resampleInterval: 5m resampleCount: 10Selection policies control which upstreams are included, not the order. Ordering within the included set is determined by the upstream scoring mechanism. See Scoring multipliers if you only want to change ordering without excluding upstreams.
Copy for your AI assistant — full selectionPolicy referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
selectionPolicy fields
| Field | Type | Default | Notes |
|---|---|---|---|
evalInterval | duration string | 1m | How often the eval function runs. Examples: 30s, 1m, 5m. |
evalFunction | JS string (YAML) or function (TS) | see default policy | JS function (upstreams, method) => upstream[]. Must return a non-empty subset (or the full array). Runs in a sobek (opens in a new tab) JS runtime. |
evalPerMethod | bool | false | When true, the eval function runs once per (network, method) pair rather than once per network. The method parameter is the actual RPC method name; when false it is always "*". |
decisionHistory | duration string | 1h | How long to retain past decisions in the admin-API ring buffer. Query recent selection decisions via erpc_project — the response includes per-network upstream scoring and recent routing decisions for the retention window you set here. |
resampleExcluded | bool | false | When true, excluded upstreams periodically receive a small number of sample requests so their metrics can refresh. Analogous to the "half-open" state of a circuit breaker. |
resampleInterval | duration string | 5m | How often an excluded upstream is given sample requests. |
resampleCount | int | 10 | Number of sample requests sent per resampling cycle. |
evalFunction — inputs and output
The function signature is:
(upstreams: Upstream[], method: string) => Upstream[]upstreams— array of all upstreams registered on the network at eval time (not filtered by the previous decision).method— the RPC method being evaluated."*"whenevalPerMethod: false; the actual method name (e.g."eth_call") whenevalPerMethod: true.- Return value — the subset of
upstreamsthat should be active. Return the full array to keep all upstreams active. Returning an empty array falls back to the full array (fail-open safety); log a warning if you rely on this.
Upstream type
type Upstream = {
id: string;
config: UpstreamConfig;
metrics: UpstreamMetrics;
};
type UpstreamConfig = {
// Upstream ID (optional, from config)
id: string;
// Arbitrary group tag — e.g. "fallback", "archive", "premium"
group: string;
// Endpoint URL, including scheme (https://, alchemy://, etc.)
endpoint: string;
};
type UpstreamMetrics = {
// p90 error rate over the last scoreMetricsWindowSize window (0.0–1.0)
errorRate: number;
// Total errors recorded (absolute counter)
errorsTotal: number;
// Total requests served (absolute counter)
requestsTotal: number;
// Rate of throttled responses (0.0–1.0)
throttledRate: number;
// p90 response time in seconds
p90ResponseSeconds: number;
// p95 response time in seconds
p95ResponseSeconds: number;
// p99 response time in seconds
p99ResponseSeconds: number;
// Blocks behind the network's highest known head (block-number delta, not seconds)
blockHeadLag: number;
// Finalized blocks behind the network's highest known finalized block
finalizationLag: number;
};Default policy behavior
If any upstream has group: "fallback", eRPC auto-creates the following selection policy (unless you define your own):
(upstreams, method) => {
const defaults = upstreams.filter(u => u.config.group !== 'fallback');
const fallbacks = upstreams.filter(u => u.config.group === 'fallback');
const maxErrorRate = parseFloat(process.env.ROUTING_POLICY_MAX_ERROR_RATE || '0.7');
const maxBlockHeadLag = parseFloat(process.env.ROUTING_POLICY_MAX_BLOCK_HEAD_LAG || '10');
const minHealthyThreshold = parseInt(process.env.ROUTING_POLICY_MIN_HEALTHY_THRESHOLD || '1');
const healthyOnes = defaults.filter(
u => u.metrics.errorRate < maxErrorRate && u.metrics.blockHeadLag < maxBlockHeadLag
);
if (healthyOnes.length >= minHealthyThreshold) {
return healthyOnes;
}
// Fall back to including everything — less harsh, prevents total blackout
return upstreams;
}ROUTING_POLICY_* environment variables
These variables tune the default policy without rewriting the eval function. They are also available inside any custom eval function via process.env:
| Variable | Default | Meaning |
|---|---|---|
ROUTING_POLICY_MAX_ERROR_RATE | 0.7 | Maximum error rate (0.0–1.0) before an upstream is excluded. |
ROUTING_POLICY_MAX_BLOCK_HEAD_LAG | 10 | Maximum block-number delta behind the network head. Chain-specific: 10 ≈ 120s on Ethereum (12s blocks), ≈ 2.5s on Arbitrum (0.25s blocks). |
ROUTING_POLICY_MIN_HEALTHY_THRESHOLD | 1 | Minimum number of healthy non-fallback upstreams required before the fallback group is excluded. If fewer healthy upstreams exist, all upstreams are returned. |
Stdlib available in the eval context
The eval runs in a sobek (opens in a new tab) ES2015+ runtime. Available globals:
process.env— access any environment variable set in the eRPC processJSON,Math,parseInt,parseFloat,Number,String,Array,Object— standard built-insconsole.log/console.error— logged at debug/error level in eRPC structured logs
No fetch, no setTimeout, no Node.js modules. The function must be synchronous and must return quickly (see pitfalls below).
Per-method evaluation (evalPerMethod: true)
When enabled, the eval function is called once for each unique RPC method seen on the network, with method set to the actual method name. This lets you apply different criteria per method — for example, exclude a slow archive node from eth_call (latency-sensitive) but include it for eth_getLogs (where range matters more than speed):
selectionPolicy:
evalInterval: 30s
evalPerMethod: true
evalFunction: |
(upstreams, method) => {
if (method === 'eth_getLogs' || method === 'trace_block') {
// For archive methods, require block availability but allow higher error rate
return upstreams.filter(u => u.metrics.blockHeadLag < 100);
}
// For everything else, tight latency + error requirements
return upstreams.filter(
u => u.metrics.errorRate < 0.5 && u.metrics.p90ResponseSeconds < 1.0
);
}Note: when evalPerMethod: true, eRPC must re-run the eval for every method that has been seen since startup. The total work per interval is numMethods × evalTime. Keep the function fast.
Resampling excluded upstreams
When resampleExcluded: true, upstreams that were excluded by the last eval decision get a "probation window" every resampleInterval. During that window, up to resampleCount requests are routed to them regardless of the policy result, so their metrics (especially errorRate) can reflect their current state.
This is optional because the EVM state poller always sends eth_getBlockByNumber("latest") to every upstream regardless of selection policy — so blockHeadLag and errorRate continue to update even for excluded upstreams.
Use resampleExcluded when you have upstreams that don't receive state-poller traffic (non-EVM chains, custom architectures) or when you want faster recovery detection.
Common pitfalls
- Returning an empty array — eRPC fails open: if the eval returns
[], all upstreams are used for that interval. This prevents a misconfigured policy from taking down the network entirely, but it also means your exclusion logic silently has no effect. Log inside the eval (console.error(...)) or check admin metrics if exclusions aren't happening. - Slow eval function — the eval blocks the scheduling goroutine for its duration. Keep it under a few milliseconds. Avoid O(n²) loops over large upstream arrays.
- Eval throws an exception — if the eval function throws, eRPC falls back to the previous decision (fail-safe) and logs the error. Check structured logs for
selectionPolicy eval errorevents. evalPerMethod: truewith many methods — each new method seen since startup adds one more eval call per interval. On a busy gateway with hundreds of distinct methods, this multiplies CPU work. Profile before enabling on high-traffic deployments.resampleCounttoo high — during resampling, excluded upstreams receive real user traffic. A high count on a badly broken upstream can increase error rates for those users. Start with10–50.- Policy not reflecting latest config — if you update
evalFunctionin config, you must restart eRPC. Live-reload of the eval function is not supported. selectionPolicyoverridesnetworkDefaults.selectionPolicyentirely — likefailsafe, if a network defines its ownselectionPolicy, the network defaults are not merged in. They are replaced wholesale.
Real-world examples
Latency-based exclusion (p90 > 2s):
selectionPolicy:
evalInterval: 30s
evalFunction: |
(upstreams, method) => {
const fast = upstreams.filter(u => u.metrics.p90ResponseSeconds < 2.0);
return fast.length > 0 ? fast : upstreams;
}Exclude archive nodes from realtime methods, include for historical:
selectionPolicy:
evalInterval: 1m
evalPerMethod: true
evalFunction: |
(upstreams, method) => {
const isHistorical = method === 'eth_getLogs'
|| method === 'trace_block'
|| method === 'debug_traceTransaction';
if (isHistorical) {
return upstreams;
}
// Exclude upstreams tagged 'archive' for realtime methods
const nonArchive = upstreams.filter(u => u.config.group !== 'archive');
return nonArchive.length > 0 ? nonArchive : upstreams;
}Error-rate + throttle combined:
selectionPolicy:
evalInterval: 1m
evalFunction: |
(upstreams, method) => {
const healthy = upstreams.filter(
u => u.metrics.errorRate < 0.5 && u.metrics.throttledRate < 0.3
);
return healthy.length > 0 ? healthy : upstreams;
}
resampleExcluded: true
resampleInterval: 2m
resampleCount: 20Append .llms.txt to this URL (or use the AI link above) to fetch the entire expanded reference as plain markdown for an AI assistant.