# Lanes & concurrency > Source: https://docs.erpc.cloud/reference/lanes > Route a class of requests to a specific provider group and eRPC computes that group's own block tip — eliminating "block not found" churn caused by cross-provider tip pollution. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Lanes & concurrency Different provider classes see different blocks at different times. When you route flashblock RPCs to one provider group and archive calls to another, eRPC automatically tracks a separate block tip for each group. The right tip reaches the right upstreams — no cross-provider contamination, no spurious `block not found` errors. What you get: - Per-group served tip decided among that group's upstreams only - Deterministic lane names on your Prometheus dashboards (`flashblocks`, `systx`, …) - Zero config — lanes emerge automatically from your `use-upstream` selectors ## Agent reference Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly: **Prompt Example #1: pin all traffic on a network to flashblocks upstreams** ```text I want all requests on my Ethereum mainnet network to route exclusively to flashblocks providers so the block tip is always flashblocks-accurate. Set directivesDefaults.useUpstream at the network level in my eRPC config, and explain when a selector qualifies for a named lane versus getting no lane gauges. Reference: https://docs.erpc.cloud/reference/lanes.llms.txt ``` **Prompt Example #2: route archive calls to a dedicated upstream group** ```text I have archive-depth upstreams tagged family:archive and I want heavy getLogs / debug trace calls to route to them while fast reads go to my premium upstreams. Configure use-upstream selectors and explain how eRPC derives a stable lane name for Prometheus dashboards. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/reference/lanes.llms.txt ``` **Prompt Example #3: debug missing lane metrics in Prometheus** ```text My Prometheus shows erpc_network_served_tip_block_number with lane=all but never a named lane label even though I'm sending X-ERPC-Use-Upstream headers. Explain the conditions that prevent a lane from forming (complex selector, fewer than 2 upstreams matched, all upstreams matched, 16-lane cap reached) and check my eRPC config for the likely cause. Reference: https://docs.erpc.cloud/reference/lanes.llms.txt ``` --- ### Lanes & concurrency — full agent reference ### How it works A **lane** is not a config object. It is an emergent artifact: when a request carries a `use-upstream` selector (via `X-ERPC-Use-Upstream` header, `?use-upstream=` query parameter, or `directivesDefaults.useUpstream` in config), eRPC resolves which upstreams match. If the matched set is a proper subset of at least two upstreams — and the selector is "simple" (see below) — eRPC derives a stable key by SHA-256-hashing the sorted upstream IDs, then lazily creates a `servedTipLane` for that key (`servedTipLaneFor`). A lane holds no pick state: only its label and, per axis, an in-process anchor for the advance-age gauge and the regression guard's memory. The network-wide pick is the lane `servedTipAll`, labelled `"all"`; only it gets the WebSocket delivered-head floor. The tip itself is the [served-tip rule](/reference/evm/block-tracking.llms.txt#served-tip) over the matched subset, computed fresh for every selector, lane or not. Nothing is shared across pods. Source: [`erpc/networks.go:L113-132`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L113-L132), [`erpc/networks.go:L628-655`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L628-L655) **LaneName algorithm.** `LaneName(ids)` sorts the upstream IDs alphabetically, then builds a set of non-empty hyphen-split tokens for each ID (empty strings from consecutive hyphens are discarded). It walks the tokens of the first sorted ID and returns the first token that appears in every other ID's token set. If no universally shared token exists, it falls back to a concatenation of the first two characters of each ID (up to five IDs). The result is deterministic and order-independent — every pod reaches the same name from the same upstream topology. Source: [`common/lane.go:L25`](https://github.com/erpc/erpc/blob/main/common/lane.go#L25) **What counts as a "simple" selector.** `isSimpleGroupSelector` rejects selectors that contain whitespace, `|`, `&`, `(`, `)`, or `,` (no boolean combinators), or more than one `!` not at position 0, or length > 128 characters. Qualifying examples: `flashblocks*`, `!flashblocks*`, `family:systx`, `alchemy-*`. Non-qualifying: `alchemy | quicknode`, `(a | b)`. **Lane lifecycle and cap.** Lanes are stored in a `sync.Map` keyed by `"grp:"` (the first 8 bytes of the SHA-256 digest encoded as 16 hex characters). On the fast path, `sync.Map.Load` returns an existing lane with no locking. On the slow path, an `atomic.Int32` counter is checked against `maxServedTipLanes = 16`; if at the cap, the request gets the same subset pick with no lane. Because the check-and-create is not atomic, up to `16 + goroutine-concurrency` lanes can transiently exist — `LoadOrStore` guarantees exactly one lane wins per key. The lane key is derived from the **matched upstream set**, not the selector text. `flashblocks*` and `fl*` targeting the same three upstreams produce the same key and share one lane. Cardinality is bounded by the actual upstream topology, not selector diversity. **No lane.** When a selector is complex, matches fewer than two upstreams, matches all upstreams, or arrives past the cap, `servedTipLaneFor` returns nil. The subset pick still applies, with no regression guard, no anchor and no gauge write, so a subset value never overwrites the `lane="all"` gauge. **UseUpstream directive precedence** (highest to lowest): 1. `X-ERPC-Use-Upstream` header on the request. 2. `?use-upstream=` query parameter. 3. `networks[*].directivesDefaults.useUpstream` config default. The selector is used in two places: to narrow the served-tip ballot (and resolve its lane), and inside the upstream selection loop (`UpstreamMatchesSelector`) to filter which upstreams receive the request. Tag-based matching is supported — `family:flashblocks` matches any upstream whose tags include that key — but negated selectors (prefix `!`) never match by tag, only by upstream ID. ### Config schema | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `networks[*].directivesDefaults.useUpstream` | `*string` | `nil` (no default selector) | Sets the `use-upstream` selector for all requests on this network when the client sends none. Identical semantics to the `X-ERPC-Use-Upstream` header. **Footgun:** a selector matching all upstreams forms no lane: its pick runs over the same upstreams as the network-wide one, but without the regression guard, the WebSocket floor or any gauge. Sources: [`common/config.go:L2109`](https://github.com/erpc/erpc/blob/main/common/config.go#L2109), [`common/request.go:L589-590`](https://github.com/erpc/erpc/blob/main/common/request.go#L589-L590) | Lanes themselves are not directly configured — they emerge from runtime request selectors and the upstream topology. They need no `servedTip` block; `servedTip.quorum` applies inside each lane as it does network-wide. ### Worked examples **1. Pin all network traffic to flashblocks upstreams.** Set `directivesDefaults.useUpstream` at the network level so every request on that chain automatically routes to flashblocks providers and gets a flashblocks-accurate tip: **Config path:** `projects[].networks[].directivesDefaults.useUpstream` **YAML — `erpc.yaml`:** ```yaml projects: - id: main networks: - architecture: evm evm: { chainId: 1 } directivesDefaults: useUpstream: "flashblocks*" # all requests → flashblocks lane ``` **TypeScript — `erpc.ts`:** ```typescript import { createConfig } from "@erpc-cloud/config"; export default createConfig({ projects: [{ id: "main", networks: [{ architecture: "evm", evm: { chainId: 1 }, directivesDefaults: { useUpstream: "flashblocks*", }, }], }], }); ``` **2. Per-request lane selection via header.** Send `X-ERPC-Use-Upstream: systx*` on individual requests to route them to system-transaction-capable upstreams. eRPC derives the lane name, computes a `systx`-scoped tip, and serves block numbers visible to that provider class only. **3. Tag-based lane targeting.** If upstreams are tagged with `family:archive`, a selector `family:archive` is simple, qualifies for a lane, and routes archive-depth calls to archive nodes — `LaneName` derives a human-readable label from the matched upstream IDs for your dashboards. **4. Complex selector routing without lane tip tracking.** A selector like `alchemy | quicknode` routes to both providers correctly via `UpstreamMatchesSelector` in the forwarding loop — but it is not simple, so no lane is created and no `lane=...` gauge appears. The tip is still computed over the two providers; this is expected behavior for multi-provider round-robin. ### Best practices - Use simple glob selectors (`flashblocks*`, `family:archive`) rather than boolean expressions when you need per-group gauges or the regression guard; boolean selectors route and scope the tip correctly but never get a lane. - Set `directivesDefaults.useUpstream` at the network level rather than injecting headers per-request — it ensures every request (including retries and hedges) carries the selector without client cooperation. - Keep the total number of distinct upstream subsets below 16 per network; beyond that, additional groups get no gauges and no regression guard. - Do not rely on `LaneName` label values being unique — two different upstream sets can produce the same label. Use the lane key (internal) as the stable identifier; the label is for human readability only. - Negated selectors (`!alchemy-*`) match by upstream ID only, not by tag. If you need tag-based exclusion, restructure as an inclusion selector on the desired tag instead. ### Edge cases & gotchas 1. **A single matched upstream does not form a lane.** `partitionKeyFor` requires `len(matched) >= 2`; a single-upstream selector gets that upstream's head and emits no gauge. 2. **Matching ALL upstreams does not form a lane.** `len(matched) >= len(all)` also falls through, preventing a match-all selector from shadowing `lane="all"`. 3. **Boolean combinator selectors are never partitioned.** `alchemy | quicknode` contains `|`; it routes and scopes the tip correctly but never gets a lane. 4. **The 16-lane cap is a soft guard.** Concurrent goroutines can transiently exceed 16. Beyond the cap, new groups get no lane. 5. **Lane key = hash of matched upstream set, not selector text.** `flashblocks*` and `fl*` matching the same upstreams share one lane. Cardinality is bounded by topology, not by selector diversity. 6. **`LaneName` is not guaranteed unique.** Two different upstream sets could produce the same name if both share the same common token. The lane key is the stable identifier; the name is decorative for dashboards. 7. **Negated selectors match by ID only, never by tag.** A pattern containing `!` skips tag evaluation entirely. `!alchemy-*` excludes by ID but will not rescue non-matching IDs via tags. 8. **`LaneName` sort is alphabetical.** For `(quicknode-systx, systx-quicknode)`, sorted order puts `quicknode-systx` first; `LaneName` returns `"quicknode"`, not `"systx"`. Source: `common/lane_test.go` (`order independent` test). 9. **Lane state is per process.** Anchors and regression-guard memory live in memory; a restarted pod rebuilds them from live heads. 10. **`LaneName` output is written to Prometheus labels as-is, without sanitization.** If upstream IDs contain unusual characters that cause `LaneName` to produce an unexpected string, that string becomes the label value verbatim. The label is never normalized or escaped beyond what `LaneName` itself produces. ### Observability Lanes are the `lane` label of the four `erpc_network_served_tip_*` series ([meanings](/reference/metrics.llms.txt#area-3-block-number-and-tip-tracking)): `lane="all"` is the network-wide pick (`servedTipAll`), `lane=` a group's own pick. A named lane's series exist only for networks that receive targeted `use-upstream` traffic; a request without a lane writes no series. Source: [`erpc/networks.go:L1502-1532`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L1502-L1532) ### Source code entry points - [`common/lane.go`](https://github.com/erpc/erpc/blob/main/common/lane.go) — `LaneName(ids []string) string`: naming algorithm, pure function. - [`common/lane_test.go`](https://github.com/erpc/erpc/blob/main/common/lane_test.go) — unit tests for `LaneName`: shared-token precedence, order-independence, fallback combo, single-id, empty input, determinism. - [`erpc/networks.go:L113-L132`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L113-L132) — `maxServedTipLanes = 16`, `servedTipLane` struct. - [`erpc/networks.go:L628-L655`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L628-L655) — `servedTipLaneFor`: lazy lane materialization with cap and `sync.Map`. - [`erpc/networks.go:L657-L746`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L657-L746) — `partitionKeyFor`: selector → SHA-256 key; `EvmUpstreamGroupForSelector`; `isSimpleGroupSelector` filter. - [`erpc/networks.go:L870-L942`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L870-L942) — `servedTip`: guard, floors and gauges per lane. - [`erpc/networks.go:L1502-L1532`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L1502-L1532) — `observeServedTipMetrics`: gauge emission per lane label. - [`common/matcher.go:L73-L112`](https://github.com/erpc/erpc/blob/main/common/matcher.go#L73-L112) — `MatchesSelector` + `UpstreamMatchesSelector`: ID + tag matching primitive. - [`common/request.go:L739-L741`](https://github.com/erpc/erpc/blob/main/common/request.go#L739-L741) — `X-ERPC-Use-Upstream` header extraction. - [`common/request.go:L811-L812`](https://github.com/erpc/erpc/blob/main/common/request.go#L811-L812) — `?use-upstream=` query-param extraction. - [`common/request.go:L1424-L1445`](https://github.com/erpc/erpc/blob/main/common/request.go#L1424-L1445) — `UpstreamMatchesSelector` call in the upstream selection loop; filters which upstreams receive the forwarded request. - [`erpc/networks.go:L617-L626`](https://github.com/erpc/erpc/blob/main/erpc/networks.go#L617-L626) — `requestSelector(ctx)`: reads `UseUpstream` directive from the bound request context for both served-tip and forwarding use. - [`common/config.go:L2109`](https://github.com/erpc/erpc/blob/main/common/config.go#L2109) — `DirectivesConfig.UseUpstream` field. - [`telemetry/metrics.go:L198-L250`](https://github.com/erpc/erpc/blob/main/telemetry/metrics.go#L198-L250) — metric definitions. ### Related pages - [Matcher](/config/matcher.llms.txt) — the `UpstreamMatchesSelector` and tag-matching primitives used to resolve which upstreams a selector targets. - [Tag-aware routing](/config/projects/selection-policies.llms.txt) — how `use-upstream` integrates with the upstream selection loop. - [Block tracking & served tip](/reference/evm/block-tracking.llms.txt) — the served-tip rule that lanes scope. - [Rate limiters](/config/rate-limiters.llms.txt) — per-upstream rate limits that interact with lane-scoped routing. --- ## Navigation (machine-readable surface) - Up: [All pages index](https://docs.erpc.cloud/llms.txt) - Root index of every page: [llms.txt](https://docs.erpc.cloud/llms.txt) · everything in one file: [llms-full.txt](https://docs.erpc.cloud/llms-full.txt) ### Sibling pages - [Error taxonomy](https://docs.erpc.cloud/reference/errors.llms.txt) — Every error eRPC can emit — typed, categorized, with retryability flags, wire HTTP status, and JSON-RPC codes — so you can interpret metrics, write alerts, and debug routing decisions confidently. - [gRPC & BDS streaming](https://docs.erpc.cloud/reference/grpc-bds.llms.txt) — Use typed protobuf APIs for block, transaction, and log lookups — eRPC routes, caches, and protects every gRPC call exactly like HTTP, with built-in deadlock defenses that keep stuck H2 streams from stalling your traffic. - [HTTP Client & Proxy Pools](https://docs.erpc.cloud/reference/http-client.llms.txt) — eRPC keeps a single pre-warmed, high-throughput connection to every upstream — and can rotate traffic across a fleet of SOCKS5 or HTTP proxies with zero extra latency. - [Metrics reference](https://docs.erpc.cloud/reference/metrics.llms.txt) — Every observable signal eRPC emits — 141 Prometheus metrics across upstreams, cache, rate limiting, consensus, hedging, and more — ready to wire into your dashboards and alerts. - [Simulator](https://docs.erpc.cloud/reference/simulator.llms.txt) — A local browser playground that runs a real eRPC instance against synthetic upstreams — explore routing, failsafe, and selection-policy behavior in seconds, no credentials needed.