Config
Selection policies

Selection Policies

AIOpen as plain markdown for AI

A 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 include
  • evalPerMethod — run the eval separately for each RPC method instead of once per network
  • resampleExcluded — 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

projectsnetworks[]selectionPolicy
erpc.yaml
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 (default 0.7) — maximum allowed error rate
  • ROUTING_POLICY_MAX_BLOCK_HEAD_LAG (default 10) — maximum blocks behind the network's highest known head (block-number delta, not seconds — 10 is ~120s on Ethereum, ~2.5s on Arbitrum)
  • ROUTING_POLICY_MIN_HEALTHY_THRESHOLD (default 1) — minimum healthy non-fallback upstreams before the fallback group is included

Fallback group example

projectsnetworks[]selectionPolicy
erpc.yaml
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: 10

Selection 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

FieldTypeDefaultNotes
evalIntervalduration string1mHow often the eval function runs. Examples: 30s, 1m, 5m.
evalFunctionJS string (YAML) or function (TS)see default policyJS 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.
evalPerMethodboolfalseWhen 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 "*".
decisionHistoryduration string1hHow 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.
resampleExcludedboolfalseWhen 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.
resampleIntervalduration string5mHow often an excluded upstream is given sample requests.
resampleCountint10Number 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. "*" when evalPerMethod: false; the actual method name (e.g. "eth_call") when evalPerMethod: true.
  • Return value — the subset of upstreams that 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:

VariableDefaultMeaning
ROUTING_POLICY_MAX_ERROR_RATE0.7Maximum error rate (0.0–1.0) before an upstream is excluded.
ROUTING_POLICY_MAX_BLOCK_HEAD_LAG10Maximum 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_THRESHOLD1Minimum 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 process
  • JSON, Math, parseInt, parseFloat, Number, String, Array, Object — standard built-ins
  • console.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 error events.
  • evalPerMethod: true with 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.
  • resampleCount too 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 with 10–50.
  • Policy not reflecting latest config — if you update evalFunction in config, you must restart eRPC. Live-reload of the eval function is not supported.
  • selectionPolicy overrides networkDefaults.selectionPolicy entirely — like failsafe, if a network defines its own selectionPolicy, 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: 20

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.