Config
Consensus

Consensus

AIOpen as plain markdown for AI

The consensus policy sends the same request to multiple upstreams in parallel and returns a result only when enough of them agree. This improves correctness, detects misbehaving nodes, and provides deterministic behavior during faults.

You can configure:

  • How many upstreams participate and how many must agree (maxParticipants, agreementThreshold)
  • What to do when upstreams disagree or too few respond (disputeBehavior, lowParticipantsBehavior)
  • Which fields to exclude from comparison (ignoreFields — useful for timestamps and chain-specific extras)
  • How to prefer certain results (preferNonEmpty, preferLargerResponses, preferHighestValueFor)
  • How to track and penalize misbehaving upstreams (punishMisbehavior, misbehaviorsDestination)
⚠️

Consensus can only be configured at network level since it requires multiple upstreams to compare results.

projectsnetworksfailsafeconsensus
erpc.yaml
projects:  - id: main    networks:      - architecture: evm        evm:          chainId: 42161        failsafe:          - matchMethod: "*"            consensus:              maxParticipants: 3              agreementThreshold: 2              disputeBehavior: acceptMostCommonValidResult              lowParticipantsBehavior: acceptMostCommonValidResult              preferNonEmpty: true              preferLargerResponses: true              punishMisbehavior:                disputeThreshold: 10                disputeWindow: 10m                sitOutPenalty: 30m

Transaction inclusion with preferHighestValueFor and fireAndForget

For reliable transaction submission, configure per-method consensus policies that pick the best values rather than requiring strict agreement:

projectsnetworksfailsafeconsensus
erpc.yaml
projects:  - id: main    networks:      - architecture: evm        evm:          chainId: 1        failsafe:          # 1. Nonce: always pick the highest to avoid 'nonce too low' errors          - matchMethod: eth_getTransactionCount            consensus:              maxParticipants: 3              agreementThreshold: 1              preferHighestValueFor:                eth_getTransactionCount: ["result"]
          # 2. Gas fees: pick highest for better inclusion during congestion          - matchMethod: eth_gasPrice|eth_maxPriorityFeePerGas            consensus:              maxParticipants: 3              agreementThreshold: 1              preferHighestValueFor:                eth_gasPrice: ["result"]                eth_maxPriorityFeePerGas: ["result"]
          # 3. Broadcast: fan-out to all nodes and return immediately          - matchMethod: eth_sendRawTransaction            consensus:              maxParticipants: 5              agreementThreshold: 1              fireAndForget: true

Misbehavior logging to S3

Export full dispute events (JSONL) to S3 for offline analysis. Each record contains the full request, all participant responses, the analysis summary, and the winner:

consensus:
  maxParticipants: 3
  agreementThreshold: 2
  misbehaviorsDestination:
    type: s3
    path: s3://my-bucket/erpc-disputes
    filePattern: "{dateByHour}/{networkId}/{method}-{instanceId}.jsonl"
    s3:
      region: us-east-1
      maxRecords: 100
      maxSize: 1048576        # 1 MB
      flushInterval: 60s
      contentType: application/jsonl
      credentials:
        mode: env             # picks AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env
Copy for your AI assistant — full consensus referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How consensus works

  1. Send the request to up to maxParticipants healthy upstreams in parallel; group identical results.
  2. If any valid group meets agreementThreshold, it wins.
  3. If no winner, apply behaviors: low participants → lowParticipantsBehavior; otherwise → disputeBehavior.
  4. Preferences (preferNonEmpty, preferLargerResponses, preferHighestValueFor) may override selection in specific contexts.
  5. Ties without preferences → dispute.
  6. All upstreams return identical error → return that error; otherwise return a low-participants error.

Performance note: consensus increases costs and latency since it waits for multiple responses. Use it selectively for critical workloads rather than all methods.

ConsensusPolicyConfig — every field

FieldTypeDefaultNotes
maxParticipantsintNumber of upstreams to query per round. The policy selects the first N healthy upstreams by score.
agreementThresholdintmaxParticipants/2 + 1Minimum upstreams that must return identical results to declare a winner.
disputeBehaviorstringreturnErrorWhat to do when upstreams disagree and no group meets threshold. See values below.
lowParticipantsBehaviorstringreturnErrorWhat to do when fewer than agreementThreshold valid responses are available. See values below.
punishMisbehaviorPunishMisbehaviorConfigOptional. Temporarily removes upstreams that consistently dispute consensus.
disputeLogLevelstringdebugVerbosity of dispute log events: trace, debug, info, warn, error.
ignoreFieldsmap[string]string[]Per-method dot-path fields excluded from canonical hash comparison.
preferNonEmptyboolfalseBias dispute resolution toward non-empty results over empty results or errors.
preferLargerResponsesboolfalseWhen multiple valid groups exist, prefer the one with the largest response body.
misbehaviorsDestinationMisbehaviorsDestinationConfigOptional. Export full dispute events to a file or S3 prefix.
preferHighestValueFormap[string]string[]Per-method field paths; picks the response with the highest numeric value at those paths. Used for nonces and gas prices.
fireAndForgetboolfalseWhen true, return immediately upon reaching agreement without cancelling remaining in-flight requests. Ideal for broadcasts like eth_sendRawTransaction.
maxWaitOnResultAdaptiveDuration{ quantile: 0.5, min: 5ms, max: 1s }After the first non-empty response arrives, cancel remaining in-flight participants at most this long after. Bounds tail latency when one slow upstream drags the whole round. See "Tail-latency caps" below.
maxWaitOnEmptyAdaptiveDuration{ quantile: 0.9, min: 50ms, max: 2s }After the first response of any kind (empty, error, or non-empty) arrives, give remaining participants at most this long. Fires before maxWaitOnResult when only empties/errors have landed so far.
requiredParticipants{ tag, minParticipants }[]Optional. Enforce a minimum number of participants carrying a given tag (e.g. region/provider diversity). Off by default. See "Tag-based participant quotas" below.

disputeBehavior values

ValueBehavior
returnErrorAlways return a dispute error when no group meets threshold.
acceptMostCommonValidResultApply preferences and select the best valid result; if no group meets threshold, return dispute.
preferBlockHeadLeaderIf the upstream with the highest block number has a non-error result, return it; otherwise fall back to acceptMostCommonValidResult.
onlyBlockHeadLeaderReturn the block head leader's non-error result if available; otherwise dispute.

The block head leader is the upstream reporting the highest block number, determined by each upstream's state poller.

lowParticipantsBehavior values

Same set of values as disputeBehavior, with one addition:

ValueBehavior
returnErrorReturn a low-participants error.
acceptMostCommonValidResultApply preferences to pick a valid result; still respects threshold semantics.
preferBlockHeadLeaderIf the block head leader has a non-error result, return it; otherwise fall back to acceptMostCommonValidResult.
onlyBlockHeadLeaderIf the leader has a non-error result, return it; if only an error, return that error; otherwise return a low-participants error.

Tag-based participant quotas (requiredParticipants)

By default consensus fills its maxParticipants slots with the first N healthy upstreams by score. requiredParticipants adds a per-tag floor on top of that: each entry says "at least minParticipants of the participants must carry tag". Use it for diversity guarantees — e.g. always include a node from a second region or a second provider in every consensus round.

The engine front-loads enough tag-matching upstreams into the participant set so the first maxParticipants drawn satisfy every entry. It does not change maxParticipants, and it leaves the order of non-required participants (their score ranking) intact — only set membership matters for voting.

consensus:
  maxParticipants: 3
  agreementThreshold: 2
  requiredParticipants:
    - tag: "region:us-*"   # glob; matched against each upstream's tags
      minParticipants: 1   # ≥1 US participant every round
    - tag: "provider:quicknode"
      minParticipants: 1   # ≥1 QuickNode participant every round

Tag the upstreams accordingly:

upstreams:
  - id: alchemy-us
    tags: [region:us-east, provider:alchemy]
  - id: quicknode-eu
    tags: [region:eu-west, provider:quicknode]
  # ...

Semantics:

  • Off by default — omit requiredParticipants and consensus behaves exactly as before.
  • Best-effort — if a required group has fewer healthy upstreams than minParticipants (or the quotas can't all fit within maxParticipants), consensus runs with what it can promote and the resulting participation is handled by your existing lowParticipantsBehavior / agreementThreshold — exactly like any organic low-participation round. No new failure mode is introduced.
  • Healthy-only — quotas are satisfied from the upstreams already eligible for this request (post selection-policy filtering), so a cordoned or excluded upstream is never forced in.
  • tag is a glob pattern (*, ?); a single upstream can satisfy multiple entries it matches.

preferNonEmpty semantics

Prioritizes meaningful data over empty responses, and empty over errors:

  • Above threshold: if both a non-empty and a consensus-valid error group meet threshold, pick the best non-empty (by count, then size).
  • Below threshold: with exactly one non-empty and at least one empty, pick the non-empty.
  • Prevents short-circuiting to empty or consensus-error when a non-empty result may still arrive.

preferLargerResponses semantics

  • Below threshold with acceptMostCommonValidResult: choose the largest non-empty.
  • Above threshold with multiple valid groups: choose the largest non-empty.
  • If a smaller non-empty meets threshold but a larger non-empty exists:
    • acceptMostCommonValidResult → choose the largest.
    • returnError → dispute (don't accept the smaller).

Tail-latency caps (maxWaitOnResult / maxWaitOnEmpty)

When one participant is consistently slow — e.g. a 10 s archive query while siblings return in 50 ms — that single laggard drags the whole request's wall-clock. The wait caps bound this after the first response arrives:

FieldTriggers when
maxWaitOnResultAt least one non-empty response is in the bag.
maxWaitOnEmptyThe first response of any kind — empty, error, or non-empty — is in the bag.

When the cap fires, the analyzer resolves with what it has using the configured disputeBehavior / lowParticipantsBehavior. In-flight participants are cancelled (or left running under fireAndForget). The earlier of the two caps wins.

Defaults (applied whenever consensus is configured):

CapDefault value
maxWaitOnResult{ quantile: 0.5, min: 5ms, max: 1s } — once any real answer is in, give the rest at most ~p50 of observed latency.
maxWaitOnEmpty{ quantile: 0.9, min: 50ms, max: 2s } — wait longer when only empties/errors have arrived, since a real answer might still land.

The quantiles read from the same per-method DDSketch the timeout policy uses. Adaptive caps self-tune across methods — eth_chainId (typical p50 ~5ms) gets a tight cap; eth_getLogs over a wide range (typical p50 ~200ms) gets a proportional one.

Static overrides when you'd rather not adapt:

consensus:
  maxParticipants:    5
  agreementThreshold: 3
  maxWaitOnResult:    200ms     # static: scalar shorthand
  maxWaitOnEmpty:     2s        # static: scalar shorthand

Custom adaptive bounds:

consensus:
  maxParticipants:    5
  agreementThreshold: 3
  maxWaitOnResult:
    quantile: 0.5
    min:      10ms
    max:      500ms
  maxWaitOnEmpty:
    quantile: 0.9
    min:      100ms
    max:      3s

The erpc_consensus_wait_capped_total{trigger} metric counts firings by trigger (result or empty); a high rate signals an upstream that should be tightened or dropped from the pool.

ignoreFields — matcher syntax

ignoreFields is a map of JSON-RPC method name → list of dot-path field patterns to exclude from canonical hash comparison. Useful for fields that legitimately differ across upstreams (timestamps, chain-specific extras).

Dot-path syntax:

  • fieldName — top-level field
  • a.b.c — nested path
  • *.fieldName — wildcard at any array index (e.g. *.blockTimestamp in an array of receipts)
  • transactions.*.gasPrice — field inside every element of a nested array

Example — real-world safe-to-ignore fields:

ignoreFields:
  eth_getLogs:
    - "*.blockTimestamp"
  eth_getTransactionReceipt:
    - "blockTimestamp"
    - "logs.*.blockTimestamp"
    - "l1Fee"
    - "l1GasPrice"
    - "l1GasUsed"
    - "gasUsedForL1"
    - "timeboosted"
    - "l1BlockNumber"
  eth_getBlockByHash:
    - "requestsHash"
    - "transactions.*.gasPrice"
    - "transactions.*.accessList"
    - "transactions.*.chainId"
    - "transactions.*.l1Fee"
    - "transactions.*.yParity"
    - "transactions.*.isSystemTx"
    - "transactions.*.depositReceiptVersion"
  eth_getBlockByNumber:
    - "requestsHash"
    - "transactions.*.gasPrice"
    - "transactions.*.accessList"
    - "transactions.*.chainId"
    - "transactions.*.l1Fee"
    - "transactions.*.yParity"
    - "transactions.*.isSystemTx"
    - "transactions.*.depositReceiptVersion"
  eth_getBlockReceipts:
    - "*.blockTimestamp"
    - "*.l1Fee"
    - "*.l1GasPrice"
    - "*.l1GasUsed"
    - "*.logs.*.blockTimestamp"

PunishMisbehaviorConfig — every field

FieldNotes
disputeThresholdNumber of disputes before an upstream is penalized (e.g. 10 = penalize after 10 strikes).
disputeWindowTime window for counting disputes (e.g. 10m). Counter resets after the window.
sitOutPenaltyHow long the upstream is excluded from consensus after hitting the threshold (e.g. 30m).

MisbehaviorsDestinationConfig — every field

FieldNotes
typefile or s3.
pathFor file: absolute path to the destination directory (must be absolute). eRPC creates it with mkdir -p semantics on boot if it doesn't exist. Each misbehavior event is appended to a file inside the directory whose name is resolved from filePattern at write time — when filePattern includes {dateByDay} or {dateByHour}, you get implicit time-based bucketing (one file per day / hour). There is no automatic file rotation; use an external log-rotation tool if you need to bound file size. For s3: s3://bucket/prefix.
filePatternFilename template. See placeholders below. Default: {timestampMs}-{method}-{networkId}.jsonl.
s3S3FlushConfig block — required when type: s3.

filePattern placeholders:

PlaceholderValue
{dateByHour}UTC hour — YYYY-MM-DD-HH
{dateByDay}UTC day — YYYY-MM-DD
{method}JSON-RPC method name
{networkId}Network ID with : replaced by _
{instanceId}Unique instance ID (auto-derived from env / pod / hostname, or generated)
{timestampMs}UTC timestamp in milliseconds — avoids key collisions on S3

Notes:

  • File writes use atomic append. Use external rotation for large volumes.
  • S3 uploads are buffered and flushed by record count, byte size, or time interval.
  • Each record is a JSONL line containing: full JSON-RPC request, all participant responses or errors, analysis summary, winner, and policy snapshot. No truncation.

S3FlushConfig — every field

FieldDefaultNotes
regionAWS region for the S3 bucket. If omitted, eRPC falls back to the AWS_REGION environment variable. If neither is set, S3 flush fails on the first attempt with a region-required error.
maxRecordsFlush buffer after this many records.
maxSizeFlush buffer after this many bytes (e.g. 1048576 for 1 MB).
flushIntervalFlush buffer after this duration even if size/count thresholds are not met (e.g. 60s).
contentTypeapplication/jsonlMIME type written to the S3 object metadata.
credentialsS3CredentialsConfig — see below.

S3 credentials modes:

modeExtra fieldsNotes
envReads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the environment. No additional fields needed.
filecredentialsFile (path to credentials file), profile (profile name within that file, optional)Reads from a standard AWS credentials file. Example: credentialsFile: /etc/erpc/aws-credentials, profile: my-profile.
secretaccessKeyID, secretAccessKeyInline credentials. Use env-var interpolation (${VAR} in YAML) to avoid committing secrets. Prefer env or file modes in production.

The same AwsAuthConfig block is used for DynamoDB cache credentials — see the DynamoDB driver reference for worked examples with each mode.

Common pitfalls

  • agreementThreshold too lowagreementThreshold: 1 means any single upstream wins, which defeats the purpose of consensus entirely (equivalent to no consensus). Use at least maxParticipants/2 + 1 for majority voting.
  • ignoreFields too aggressive — ignoring structural fields like blockHash or transactionIndex can mask real disagreements. Only ignore fields you have verified are non-deterministic across correct nodes.
  • S3 costs at high dispute rates — if many methods dispute frequently, misbehavior logging can generate substantial S3 traffic. Start with file type and switch to S3 after validating dispute volume.
  • disputeBehavior: returnError in production — this surfaces disputes as errors to clients, which can cause retries and amplify load. Prefer acceptMostCommonValidResult unless you specifically need strict failure signaling.
  • fireAndForget: true on read methods — designed for broadcast writes. On read methods, it causes unnecessary in-flight requests that consume upstream quota without benefiting the client.
  • Forgetting to scale maxParticipants with upstream count — if you have only 2 upstreams but set maxParticipants: 3, the policy silently proceeds with 2 and requires both to agree (or triggers lowParticipantsBehavior).
  • punishMisbehavior with a very short disputeWindow — a 1-second window combined with a low disputeThreshold can cause flapping. Use windows of minutes and thresholds of 10+ for production.

Real-world examples

Multi-chain correctness check — require majority agreement:

failsafe:
  - matchMethod: "eth_call|eth_getBalance|eth_getLogs"
    consensus:
      maxParticipants: 3
      agreementThreshold: 2
      disputeBehavior: acceptMostCommonValidResult
      preferNonEmpty: true
      ignoreFields:
        eth_getLogs: ["*.blockTimestamp"]
      punishMisbehavior:
        disputeThreshold: 5
        disputeWindow: 5m
        sitOutPenalty: 15m

Archive node — accept best available when data is sparse:

failsafe:
  - matchMethod: "eth_getBlockByNumber|eth_getBlockByHash"
    consensus:
      maxParticipants: 2
      agreementThreshold: 1
      disputeBehavior: preferBlockHeadLeader
      ignoreFields:
        eth_getBlockByNumber:
          - "requestsHash"
          - "transactions.*.accessList"

Transaction pipeline — nonce, gas, broadcast:

See the transaction inclusion example at the top of this page.

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.