FAQ
AI agents: fetch https://docs.erpc.cloud/faq.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /faq.llms.txt

FAQ

Get unstuck fast. These are the questions that come up most often when setting up, configuring, and operating eRPC.

How do I use environment variables in my config file?

YAML: every $VAR or ${VAR} is substituted before the file is parsed — no extra syntax. TypeScript: use process.env.MY_KEY like any Node program.

Which config file does eRPC load automatically?

eRPC checks for erpc.yaml, .yml, .ts, and .js — first in ./, then /, then /root/. First file found wins. No config file found and --require-config not set? eRPC starts with a built-in default project.

What happens if I leave out a config section — do I get defaults?

Yes. SetDefaults fills every missing field. Run erpc dump ./erpc.yaml to see the exact config the engine would use, with all defaults resolved.

How do I validate my config without starting eRPC?

erpc validate ./erpc.yaml

Exit 0 means no errors. Pipe stdout through jq .errors to see only errors.

Can I use --set to override a single config value?

No. The --set / -s flag was planned but is not implemented. Use environment variables or edit the config file.


Why does eRPC still hit upstreams even though I added a cache?

The most common cause: no finality: realtime policy. Methods like eth_blockNumber and eth_gasPrice resolve to realtime finality and only match policies that explicitly declare finality: realtime. A policy without finality silently covers only finalized data.

Add a second policy for realtime:

database.evmJsonRpcCache
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: my-redis        driver: redis        redis:          addr: "localhost:6379"    policies:      - connector: my-redis        network: "*"        method: "*"        finality: finalized      - connector: my-redis        network: "*"        method: "*"        # realtime methods (eth_blockNumber, eth_gasPrice) only match this policy        finality: realtime        ttl: 2s

Why do empty eth_getLogs results trigger a retry instead of being cached?

The default empty: ignore skips caching empty results. This is intentional — an empty log set for a block that hasn't been produced yet would be wrong to cache. Set empty: allow if you genuinely want to cache empty responses for confirmed blocks.

How does the realtime TTL guard work?

It checks the block's age, not how long ago the response was cached. A response cached five minutes ago is still served if its block is fresh; a response cached ten seconds ago is rejected if its block is older than the TTL.

Is zstd compression on by default?

Yes — even if you omit the compression: block entirely. To disable it you must explicitly set compression.enabled: false.

How do I disable caching entirely?

Omit the database.evmJsonRpcCache key (and database.svmJsonRpcCache if you run SVM networks). When the pointer is nil, that cache subsystem is never created.

Does eRPC support Solana?

Yes. Declare architecture: svm with an svm.cluster, give each upstream type: svm (it is never inferred), and eRPC applies the same failsafe, routing, and caching machinery it uses for EVM. Network ids are svm:<cluster> — or svm:<chain>:<cluster> for forks like Fogo and Eclipse. See Networks and the SVM reference.

Why isn't my Solana getBalance cached even at commitment: finalized?

Because Solana's finalized is the state at the latest rooted slot, and that slot advances roughly every 400 ms — it is a moving head, not an immutability guarantee. Only reads pinned to a specific slot or signature (getBlock, getTransaction) at finalized commitment are cached as immutable; every other read is realtime and needs a finality: realtime policy with a TTL. Full explanation: SVM JSON-RPC cache.


What auth strategies does eRPC support?

Five: secret (static token), jwt (signed bearer), siwe (EIP-4361 Ethereum sign-in), network (IP/CIDR allowlist), and database (dynamic lookup against PostgreSQL, DynamoDB, Redis, or memory). If projects[*].auth is nil, all requests are allowed.

How does the database strategy handle a DB outage?

Default is fail-closed (failOpen.enabled: false), so any DB error rejects the request. Set failOpen.enabled: true to treat DB failures as successful authentication using a synthetic failOpen.userId. A circuit-breaker limits live DB probes to one per second during an outage.

Why is my JWT auth rejecting all tokens?

Most likely: empty verificationKeys map. With no keys configured no signature can be verified and every JWT is rejected. Add at least one kid → keyData entry.

How do rate-limit budgets interact with auth?

After successful authentication the rate-limit check fires at auth scope before the request reaches project or network scope. A per-user budget on the auth record overrides the strategy-level budget.


What is the default rate-limiting behavior?

When rateLimiters is omitted, no budgets are registered and every request passes through. Adding rateLimiters: without any budgets: entries also passes all traffic.

Does eRPC rate-limit inbound or outbound traffic?

Both. Check order: auth → project → network → upstream. Inbound rejections return HTTP 429. Upstream-level rejections return HTTP 200 with JSON-RPC error code -32005.

Why does my upstream budget rate limit return HTTP 200, not 429?

ErrUpstreamRateLimitRuleExceeded is not in the wire 429 list. It returns HTTP 200 with JSON-RPC error code -32005.

Can multiple eRPC instances share a rate-limit budget?

Yes, but only with store.driver: redis. The default memory driver is per-process — each instance counts independently. With Redis, set distinct cacheKeyPrefix values per deployment to prevent quota collision.

Does the auto-tuner affect shared budgets?

Yes, and it can conflict. Each upstream has its own auto-tuner that independently adjusts MaxCount on the shared budget's rules. Whichever tuner fires last wins.


Why do I see -32603 for errors that should have a specific code?

-32603 is the fallback for any internal error that doesn't map to a specific JSON-RPC code. Check the error message field and look for an ErrCode in the body. If the error is wrapped in ErrUpstreamsExhausted, the dominant child error determines the final code.

What does -32005 mean?

-32005 is JsonRpcErrorCapacityExceeded — eRPC's normalized code for rate limits, upstream 429s, billing issues, and endpoint capacity errors.

What does -32014 mean?

-32014 is JsonRpcErrorMissingData — the requested block, transaction, or state is not available on this node (archive vs. full node mismatch, or data not yet indexed).

Why does eth_sendRawTransaction sometimes get retried to a different upstream?

eth_sendRawTransaction is intentionally excluded from the non-retryable write-method guard. Reverts and out-of-gas errors are safe to retry because the transaction was never mined. However, "already known" or "nonce too low" responses are classified as ErrEndpointNonceException and are NOT retried to another upstream.

How do I suppress verbose error labels in Prometheus?

Set metrics.errorLabelMode: compact (the default). Compact mode emits only the error code chain; verbose mode includes the full message string, which can create high-cardinality series.


FAQ — agent detailsExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How it works

The FAQ answers above are each grounded in one or more KB source files. This section provides the full mechanics, config schema, worked examples, best practices, edge cases, and source entry points for each topic area.

Config schema

Key fields referenced across FAQ answers:

YAML pathTypeDefaultNotes
logLevelstring"INFO"Zerolog global level; also overridable via LOG_LEVEL env var (applied twice: at init and after config load). common/defaults.go:L50-52
clusterKeystring"erpc-default"Logical replica group for shared-state scoping. common/defaults.go:L53-55
metrics.errorLabelModestring"compact""compact" = code-only labels; "verbose" = code + message. High-cardinality risk with verbose. common/defaults.go:L762-764
database.evmJsonRpcCache.policies[*].finalitystring"finalized" (zero value)Must be explicitly "realtime" to cache realtime methods. Omitting is identical to finality: finalized. common/data.go:L10
database.evmJsonRpcCache.policies[*].emptystring"ignore""ignore" = skip empty on write, treat as miss on read. "allow" = cache empty results. "only" = cache only empty results.
database.evmJsonRpcCache.compression.enabledbooltrueAuto-enabled even when compression: block is omitted. Must explicitly be set to false to opt out. common/defaults.go:L580-582
rateLimiters.store.driverstring"memory""redis" required for cross-instance shared budgets. common/defaults.go:L2782
rateLimiters.store.cacheKeyPrefixstring"erpc_rl_"Must differ between eRPC deployments sharing the same Redis to prevent quota collision. upstream/ratelimiter_registry.go:L76
rateLimiters.budgets[*].rules[*].maxCountuint32required0 means always-blocked (not unlimited). Omit rateLimitBudget entirely for unrestricted access.
projects[*].auth*AuthConfignil (allow-all)When nil, all requests are authenticated as anonymous with no check.
projects[*].auth.strategies[*].database.failOpen.enabledboolfalseSet true to allow traffic through on DB outage, using the synthetic failOpen.userId. common/defaults.go:L2735-2742

Worked examples

1. Full cache setup with finality buckets. Cover both finalized and realtime data; without the second policy, eth_blockNumber and eth_gasPrice will always miss cache and hit upstreams:

database.evmJsonRpcCache
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: my-redis        driver: redis        redis:          addr: "localhost:6379"    policies:      - connector: my-redis        network: "*"        method: "*"        finality: finalized      - connector: my-redis        network: "*"        method: "*"        finality: realtime        ttl: 2s

2. JWT auth with per-user rate-limit budgets. The JWT claim named by rateLimitBudgetClaimName (default "rlm") sets the per-user budget; tokens without that claim fall through to the strategy-level budget or no budget:

projects[].auth
erpc.yaml
auth:  strategies:    - type: jwt      jwt:        verificationKeys:          my-key-id: "file:///run/secrets/jwt-pub.pem"        allowedAlgorithms: ["RS256"]      rateLimitBudget: default-budget

3. Redis shared rate limiter with separate prefixes per environment. Prevents staging and production counters from colliding when both deployments share one Redis instance:

rateLimiters
erpc.yaml
rateLimiters:  store:    driver: redis    redis:      addr: "shared-redis:6379"    cacheKeyPrefix: "erpc_prod_"  budgets:    - id: provider-budget      rules:        - method: "*"          maxCount: 500          period: second

4. Database auth with fail-open. Lets traffic through during a DB outage, capping emergency users with a dedicated budget:

projects[].auth.strategies[]
erpc.yaml
strategies:  - type: database    database:      connector:        driver: postgresql        postgresql:          connectionUri: "${AUTH_DB_URI}"      failOpen:        enabled: true        userId: emergency-failopen        rateLimitBudget: failopen-budget

Request/response behavior

Config loading flow. os.ExpandEnv runs on raw YAML bytes before the YAML parser sees the file. If $VAR expands to a value containing : or {, the value must be quoted: endpoint: "${MY_URL}". TypeScript configs receive the full OS environment as process.env in the sobek runtime — substitution is JS-native, not a pre-parse pass. common/config.go:L102

Cache finality matching. On reads, a finalized request also matches unfinalized policies (a block finalized after write may have been stored unfinalized). A realtime request matches only realtime policies — no cross-finality fallthrough. On writes, finality must match exactly. architecture/evm/json_rpc_cache.go:L951-978

Realtime TTL guard. Compares the block's unix timestamp (from the response body) against time.Now(). It does NOT check how long ago the response was written to cache. For methods with no block timestamp in the response (eth_blockNumber, eth_gasPrice, eth_getLogs), the guard falls back to the gRPC connector's latest-block timestamp, which is polled every 60 seconds. If no timestamp is available the guard fails open and serves the cached result. architecture/evm/json_rpc_cache.go:L841-920

Rate-limit check order. auth → project → network → upstream. Each layer uses the same TryAcquirePermit path. Auth, project, and network rejections return HTTP 429. Upstream rejections return HTTP 200 with JSON-RPC code -32005 because ErrUpstreamRateLimitRuleExceeded is absent from the wire 429 switch. erpc/http_server.go:L1484

Error code derivation. When ErrUpstreamsExhausted wraps multiple per-upstream errors, the final JSON-RPC code is the dominant child: highest occurrence count, skipping ErrCodeUpstreamRequestSkipped and ErrCodeEndpointUnsupported; earliest error per code wins ties. The fallback for any unmapped internal error is -32603. common/json_rpc.go:L1501

Best practices

  • Always add a finality: realtime policy alongside your finality: finalized policy — omitting it silently bypasses the cache for all time-sensitive methods. The zero-value trap is the single most common cache misconfiguration. On SVM this is even more pronounced: realtime covers nearly all traffic, because only getBlock/getTransaction at finalized commitment (plus getBlockTime and getInflationReward) land in the finalized bucket.
  • Use erpc dump ./erpc.yaml before deploying to verify that all defaults have been filled in as expected, especially compression and auto-tuner settings.
  • Use erpc validate ./erpc.yaml in CI; exit code 0 means no errors. Pipe through jq .errors for clean output.
  • For fleet deployments using Redis rate limiting, set a distinct cacheKeyPrefix per environment (erpc_prod_, erpc_staging_) to prevent quota collision between deployments sharing one Redis.
  • Set metrics.errorLabelMode: compact (the default) in production. Verbose mode can create unbounded label cardinality when provider error messages contain addresses, hashes, or block numbers.
  • For database auth strategy, set failOpen.enabled: true with a dedicated failOpen.rateLimitBudget so traffic during DB outages is capped and observable — the circuit-breaker limits live DB probes to one per second automatically.
  • Do not set rateLimitBudget to a budget that doesn't exist in rateLimiters.budgets — the error is a runtime error, not a startup error, and will surface only on the first request that reaches that code path.

Edge cases & gotchas

  1. finality zero-value trap. Omitting finality in a cache policy is identical to finality: finalized. Realtime methods never match and always hit upstreams silently. common/data.go:L10
  2. Compression is always on. The compression sub-block is auto-created with enabled: true even when omitted. You must explicitly write compression.enabled: false to disable it. common/defaults.go:L482-488
  3. Empty eth_getLogs with empty: ignore triggers retries. The default behavior skips caching empty results. The upstream is hit every time until a non-empty response is produced or the block is confirmed past the network head. architecture/evm/json_rpc_cache.go:L1079
  4. Realtime TTL guard checks block age, not cache-entry age. A cached response from 10 minutes ago is served if its block is still fresh; a cached response from 5 seconds ago is rejected if its block is stale.
  5. Memory rate limiter is per-process. In a multi-instance deployment, each instance has independent counters. The effective limit scales linearly with instance count. Use the Redis driver for shared limits. upstream/ratelimiter_registry.go:L48-51
  6. Upstream rate-limit rejections return HTTP 200. ErrUpstreamRateLimitRuleExceeded is not in the 429 wire list. Client SDKs that interpret HTTP 200 as success will not retry — check the JSON-RPC error code -32005. erpc/http_server.go:L1484
  7. maxCount: 0 in a rate-limit rule means always-blocked, not unlimited. Omit rateLimitBudget entirely to allow unrestricted traffic. upstream/ratelimiter_budget.go:L109-119
  8. --set / -s is not implemented. Passing -s or --set produces "flag provided but not defined: -s" from the CLI framework — no eRPC code runs. cmd/erpc/main.go:L86-90
  9. --config sets requireConfig: true implicitly. If the specified file does not exist, eRPC exits 1001. There is no fallback to auto-discovery when an explicit path is given. cmd/erpc/main.go:L300-302
  10. YAML strict mode rejects unknown keys. A typo in any YAML key is a fatal error at startup. TypeScript configs go through the same strict YAML decoder via JSON round-trip. common/config.go:L103
  11. Auto-tuner is implicitly enabled when rateLimitBudget is set. SetDefaults auto-creates RateLimitAutoTuneConfig when rateLimitBudget != "" and rateLimitAutoTune is nil. Opt out with rateLimitAutoTune.enabled: false. common/defaults.go:L1674
  12. Database auth circuit-breaker limits probes to 1 per second during outage. Only one goroutine per second is elected to probe the real DB path; all others get fail-open (if enabled) immediately. auth/strategy_database.go:L25-54
  13. Empty verificationKeys rejects all JWTs. With no keys, findVerificationKey always fails → every JWT token is rejected regardless of content. Add at least one kid → keyData entry. auth/strategy_jwt.go:L25-42
  14. os.ExpandEnv in YAML runs on raw bytes before parse. A YAML value like password: ${REDIS_PASSWORD} is substituted before decode. If the value contains YAML special characters (:, {), the resulting YAML will be malformed — use quoted values: password: "${REDIS_PASSWORD}". common/config.go:L102
  15. SVM cache keys are case-sensitive; EVM's are not. The EVM hasher lowercases string params, which is correct for hex. The SVM key preserves case exactly, because base58 pubkeys and signatures are case-significant and folding them would serve one account another's data. architecture/svm/json_rpc_cache.go:L412-433
  16. SVM has no realtime block-age guard. The TTL on a finality: realtime SVM policy is the only staleness bound — there is no block-timestamp cross-check to catch an over-long TTL, and erpc_cache_get_age_guard_reject_total never fires on SVM networks. architecture/svm/json_rpc_cache.go:L30-33
  17. type: svm is never inferred on an upstream. An upstream omitting type defaults to evm, so a Solana endpoint without it fails bootstrap while trying eth_chainId. common/defaults.go:L1715-1718

Observability

MetricTypeWhen it fires
erpc_cache_get_age_guard_reject_totalcounterRealtime cached result rejected because block age exceeded policy TTL
erpc_cache_set_skipped_totalcountershouldCacheResponse returned false (empty result, size filter, future block)
erpc_rate_limits_totalcounterAny budget rule denied a request; labeled by origin (auth/project/network/upstream)
erpc_rate_limiter_failopen_totalcounterRate limiter allowed request due to Redis timeout or admission semaphore full
erpc_auth_failed_totalcounterAuth strategy rejected the request
erpc_upstream_request_errors_totalcounterUpstream returned an error; error label uses ErrorFingerprint

Source code entry points

Related pages