/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.yamlExit 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: 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: 2sWhy 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 path | Type | Default | Notes |
|---|---|---|---|
logLevel | string | "INFO" | Zerolog global level; also overridable via LOG_LEVEL env var (applied twice: at init and after config load). common/defaults.go:L50-52 |
clusterKey | string | "erpc-default" | Logical replica group for shared-state scoping. common/defaults.go:L53-55 |
metrics.errorLabelMode | string | "compact" | "compact" = code-only labels; "verbose" = code + message. High-cardinality risk with verbose. common/defaults.go:L762-764 |
database.evmJsonRpcCache.policies[*].finality | string | "finalized" (zero value) | Must be explicitly "realtime" to cache realtime methods. Omitting is identical to finality: finalized. common/data.go:L10 |
database.evmJsonRpcCache.policies[*].empty | string | "ignore" | "ignore" = skip empty on write, treat as miss on read. "allow" = cache empty results. "only" = cache only empty results. |
database.evmJsonRpcCache.compression.enabled | bool | true | Auto-enabled even when compression: block is omitted. Must explicitly be set to false to opt out. common/defaults.go:L580-582 |
rateLimiters.store.driver | string | "memory" | "redis" required for cross-instance shared budgets. common/defaults.go:L2782 |
rateLimiters.store.cacheKeyPrefix | string | "erpc_rl_" | Must differ between eRPC deployments sharing the same Redis to prevent quota collision. upstream/ratelimiter_registry.go:L76 |
rateLimiters.budgets[*].rules[*].maxCount | uint32 | required | 0 means always-blocked (not unlimited). Omit rateLimitBudget entirely for unrestricted access. |
projects[*].auth | *AuthConfig | nil (allow-all) | When nil, all requests are authenticated as anonymous with no check. |
projects[*].auth.strategies[*].database.failOpen.enabled | bool | false | Set 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: 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: 2s2. 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:
auth: strategies: - type: jwt jwt: verificationKeys: my-key-id: "file:///run/secrets/jwt-pub.pem" allowedAlgorithms: ["RS256"] rateLimitBudget: default-budget3. Redis shared rate limiter with separate prefixes per environment. Prevents staging and production counters from colliding when both deployments share one Redis instance:
rateLimiters: store: driver: redis redis: addr: "shared-redis:6379" cacheKeyPrefix: "erpc_prod_" budgets: - id: provider-budget rules: - method: "*" maxCount: 500 period: second4. Database auth with fail-open. Lets traffic through during a DB outage, capping emergency users with a dedicated budget:
strategies: - type: database database: connector: driver: postgresql postgresql: connectionUri: "${AUTH_DB_URI}" failOpen: enabled: true userId: emergency-failopen rateLimitBudget: failopen-budgetRequest/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: realtimepolicy alongside yourfinality: finalizedpolicy — 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:realtimecovers nearly all traffic, because onlygetBlock/getTransactionatfinalizedcommitment (plusgetBlockTimeandgetInflationReward) land in thefinalizedbucket. - Use
erpc dump ./erpc.yamlbefore deploying to verify that all defaults have been filled in as expected, especially compression and auto-tuner settings. - Use
erpc validate ./erpc.yamlin CI; exit code 0 means no errors. Pipe throughjq .errorsfor clean output. - For fleet deployments using Redis rate limiting, set a distinct
cacheKeyPrefixper 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
databaseauth strategy, setfailOpen.enabled: truewith a dedicatedfailOpen.rateLimitBudgetso traffic during DB outages is capped and observable — the circuit-breaker limits live DB probes to one per second automatically. - Do not set
rateLimitBudgetto a budget that doesn't exist inrateLimiters.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
finalityzero-value trap. Omittingfinalityin a cache policy is identical tofinality: finalized. Realtime methods never match and always hit upstreams silently.common/data.go:L10- Compression is always on. The
compressionsub-block is auto-created withenabled: trueeven when omitted. You must explicitly writecompression.enabled: falseto disable it.common/defaults.go:L482-488 - Empty
eth_getLogswithempty: ignoretriggers 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 - 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.
- 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 - Upstream rate-limit rejections return HTTP 200.
ErrUpstreamRateLimitRuleExceededis 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 maxCount: 0in a rate-limit rule means always-blocked, not unlimited. OmitrateLimitBudgetentirely to allow unrestricted traffic.upstream/ratelimiter_budget.go:L109-119--set/-sis not implemented. Passing-sor--setproduces"flag provided but not defined: -s"from the CLI framework — no eRPC code runs.cmd/erpc/main.go:L86-90--configsetsrequireConfig: trueimplicitly. 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- 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 - Auto-tuner is implicitly enabled when
rateLimitBudgetis set.SetDefaultsauto-createsRateLimitAutoTuneConfigwhenrateLimitBudget != ""andrateLimitAutoTuneis nil. Opt out withrateLimitAutoTune.enabled: false.common/defaults.go:L1674 - 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 - Empty
verificationKeysrejects all JWTs. With no keys,findVerificationKeyalways fails → every JWT token is rejected regardless of content. Add at least onekid → keyDataentry.auth/strategy_jwt.go:L25-42 os.ExpandEnvin YAML runs on raw bytes before parse. A YAML value likepassword: ${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- 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 - SVM has no realtime block-age guard. The TTL on a
finality: realtimeSVM policy is the only staleness bound — there is no block-timestamp cross-check to catch an over-long TTL, anderpc_cache_get_age_guard_reject_totalnever fires on SVM networks.architecture/svm/json_rpc_cache.go:L30-33 type: svmis never inferred on an upstream. An upstream omittingtypedefaults toevm, so a Solana endpoint without it fails bootstrap while tryingeth_chainId.common/defaults.go:L1715-1718
Observability
| Metric | Type | When it fires |
|---|---|---|
erpc_cache_get_age_guard_reject_total | counter | Realtime cached result rejected because block age exceeded policy TTL |
erpc_cache_set_skipped_total | counter | shouldCacheResponse returned false (empty result, size filter, future block) |
erpc_rate_limits_total | counter | Any budget rule denied a request; labeled by origin (auth/project/network/upstream) |
erpc_rate_limiter_failopen_total | counter | Rate limiter allowed request due to Redis timeout or admission semaphore full |
erpc_auth_failed_total | counter | Auth strategy rejected the request |
erpc_upstream_request_errors_total | counter | Upstream returned an error; error label uses ErrorFingerprint |
Source code entry points
cmd/erpc/main.go(opens in a new tab) — config search order,--configflag,--set(commented out),getConfig,baseCliActioncommon/defaults.go(opens in a new tab) —SetDefaults, default project synthesis, auto-tune injection, compression auto-creationcommon/config.go(opens in a new tab) —LoadConfig,os.ExpandEnvYAML substitution, TS loader, strict YAML decodercommon/data.go(opens in a new tab) —DataFinalityStateenum (finalized=0, realtime=2)architecture/evm/json_rpc_cache.go(opens in a new tab) —shouldAcceptCachedResult(realtime age guard),shouldCacheResponse, finality policy matchingerpc/http_server.go(opens in a new tab) — wire HTTP status switch, 429 error list, upstream-level 200 returnupstream/ratelimiter_budget.go(opens in a new tab) —TryAcquirePermit, fail-open, admission semaphoreauth/strategy_database.go(opens in a new tab) — circuit-breaker,tryFastFailOpen, singleflight dedupcommon/errors.go(opens in a new tab) — full error type hierarchy, retryability flags,ErrUpstreamsExhausteddominant-code selectionarchitecture/evm/error_normalizer.go(opens in a new tab) — vendor error normalization rules, 22-rule cascade
Related pages
- Cache database — connector setup, full policy reference, compression config.
- Auth — all five strategies in depth, method filtering, per-user budgets.
- Rate limiters — budget rules, auto-tuner, Redis vs. memory store.
- Config reference — complete field listing with defaults.
- Survive provider outages — putting failsafe and routing together.