# Selection Policies > Source: https://docs.erpc.cloud/config/projects/selection-policies > Selection policies control which upstreams are eligible to serve traffic by running a JS eval function on a periodic interval — like a healthcheck that gates routing. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Selection Policies 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 **Config path:** `projects > networks[] > selectionPolicy` **YAML — `erpc.yaml`:** ```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; } ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ 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; }, }, }], }], }); ``` > **INFO** > 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 **Config path:** `projects > networks[] > selectionPolicy` **YAML — `erpc.yaml`:** ```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 ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ 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, }, }], }], }); ``` > **INFO** > 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](/config/projects/upstreams.llms.txt#customizing-scores--priorities) if you only want to change ordering without excluding upstreams. --- ### Copy for your AI assistant — full selectionPolicy reference ### `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](https://github.com/dop251/sobek) 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`](/operation/admin.llms.txt#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: ```ts (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 ```ts 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): ```js (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](https://github.com/dop251/sobek) 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): ```yaml 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):** ```yaml 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:** ```yaml 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:** ```yaml 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 ``` --- > **TIP** > 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.