/reference/lanes.llms.txt
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-upstreamselectors
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
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
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
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 referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
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, the regression guard's memory and, on finalized, the finalized floor. The network-wide pick is the lane servedTipAll, labelled "all". The tip itself is the served-tip rule over the matched subset, computed fresh for every selector, lane or not. Only the network-wide lane's finalized floor is shared across pods. Source: erpc/networks.go:L113-132, erpc/networks.go:L628-655
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
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:<hex16>" (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.
Method subsets. Only upstreams that serve the request's method (ignoreMethods/allowMethods) vote on its served tip. A request whose method every upstream serves, or none does, uses its selector's lane. One whose method only some upstreams serve forms no lane of its own: it reads its selector lane's view capped at the block servedTip.quorum of the serving upstreams hold, with the stateless pick (no guard, no finalized floor, no gauges). Each method's voter set is built on its first request and rebuilt when the network's upstreams or an upstream's method filter change; a request reads it with one lookup. Only method names the config or the built-in tables define exactly are cached; a casing variant (ETH_CALL) or an unknown name is resolved per request.
No lane. When a selector is complex, matches fewer than two upstreams, matches all upstreams, or arrives past the cap, laneFor 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):
X-ERPC-Use-Upstreamheader on the request.?use-upstream=<value>query parameter.networks[*].directivesDefaults.useUpstreamconfig 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 or any gauge. Sources: common/config.go:L2109, common/request.go:L589-590 |
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:
projects: - id: main networks: - architecture: evm evm: { chainId: 1 } directivesDefaults: useUpstream: "flashblocks*" # all requests → flashblocks lane2. 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.useUpstreamat 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
LaneNamelabel 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
- A single matched upstream does not form a lane.
partitionKeyForrequireslen(matched) >= 2; a single-upstream selector gets that upstream's head and emits no gauge. - Matching ALL upstreams does not form a lane.
len(matched) >= len(all)also falls through, preventing a match-all selector from shadowinglane="all". - Boolean combinator selectors are never partitioned.
alchemy | quicknodecontains|; it routes and scopes the tip correctly but never gets a lane. - The 16-lane cap is a soft guard. Concurrent goroutines can transiently exceed 16. Beyond the cap, new groups get no lane.
- Lane key = hash of matched upstream set, not selector text.
flashblocks*andfl*matching the same upstreams share one lane. Cardinality is bounded by topology, not by selector diversity. LaneNameis 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.- 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. LaneNamesort is alphabetical. For(quicknode-systx, systx-quicknode), sorted order putsquicknode-systxfirst;LaneNamereturns"quicknode", not"systx". Source:common/lane_test.go(order independenttest).- Lane state is per process. Anchors, regression-guard memory and finalized floors live in memory; a restarted pod rebuilds them from live heads. The network-wide lane's finalized floor is also shared across pods, so a restarted pod serves it again once one of its voters reports it.
LaneNameoutput is written to Prometheus labels as-is, without sanitization. If upstream IDs contain unusual characters that causeLaneNameto produce an unexpected string, that string becomes the label value verbatim. The label is never normalized or escaped beyond whatLaneNameitself produces.
Observability
Lanes are the lane label of the four erpc_network_served_tip_* series (meanings): lane="all" is the network-wide pick (servedTipAll), lane=<LaneName> 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:L1647-1677
Source code entry points
common/lane.go(opens in a new tab) —LaneName(ids []string) string: naming algorithm, pure function.common/lane_test.go(opens in a new tab) — unit tests forLaneName: shared-token precedence, order-independence, fallback combo, single-id, empty input, determinism.erpc/networks.go:L117-L137(opens in a new tab) —maxServedTipLanes = 16,servedTipLanestruct.erpc/networks.go:L640-L667(opens in a new tab) —servedTipLaneFor: lazy lane materialization with cap andsync.Map.erpc/networks.go:L669-L758(opens in a new tab) —partitionKeyFor: selector → SHA-256 key;EvmUpstreamGroupForSelector;isSimpleGroupSelectorfilter.erpc/networks.go:L883-L971(opens in a new tab) —servedTip: guard, floors and gauges per lane.erpc/networks.go(opens in a new tab) —bindLaneGauges,servedTipGauges.observe: gauges bound once per lane label.common/matcher.go:L73-L112(opens in a new tab) —MatchesSelector+UpstreamMatchesSelector: ID + tag matching primitive.common/request.go:L739-L741(opens in a new tab) —X-ERPC-Use-Upstreamheader extraction.common/request.go:L811-L812(opens in a new tab) —?use-upstream=query-param extraction.common/request.go:L1424-L1445(opens in a new tab) —UpstreamMatchesSelectorcall in the upstream selection loop; filters which upstreams receive the forwarded request.erpc/networks.go:L630-L639(opens in a new tab) —requestSelector(ctx): readsUseUpstreamdirective from the bound request context for both served-tip and forwarding use.common/config.go:L2109(opens in a new tab) —DirectivesConfig.UseUpstreamfield.telemetry/metrics.go:L198-L250(opens in a new tab) — metric definitions.
Related pages
- Matcher — the
UpstreamMatchesSelectorand tag-matching primitives used to resolve which upstreams a selector targets. - Tag-aware routing — how
use-upstreamintegrates with the upstream selection loop. - Block tracking & served tip — the served-tip rule that lanes scope.
- Rate limiters — per-upstream rate limits that interact with lane-scoped routing.