Config
Database
Drivers
AI agents: fetch https://docs.erpc.cloud/config/database/drivers.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: /config/database/drivers.llms.txt

Storage drivers

Pick the back-end that fits your deployment — in-process LRU for a single node, Redis for multi-instance scale, PostgreSQL or DynamoDB for managed persistence, or a read-only gRPC BDS layer for historical chain data. Swap drivers by changing one line. Wrap any connector with retry, circuit-breaker, hedge, or timeout policies so a slow cache never slows down your upstream calls.

Connectors are architecture-neutral. The examples on this page use database.evmJsonRpcCache.connectors[]; the identical connector schema lives under database.svmJsonRpcCache.connectors[] for SVM networks, and both caches can reference the same physical backend — partition keys are prefixed with the network id (evm:1, svm:mainnet-beta), so the keyspaces are disjoint. See SVM JSON-RPC cache.

Quick taste

Illustrative, not a tuned production config — add a Redis connector:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: my-redis        # swap one line to change back-end: memory | redis | postgresql | dynamodb | grpc        driver: redis        redis:          uri: redis://localhost:6379/0          connPoolSize: 8

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: add a Redis cache for multi-instance deployments
I'm running eRPC on multiple pods and need a shared Redis cache so all instances
share cached responses. Add a Redis connector with a sensible connection pool,
per-operation timeouts, a circuit-breaker on Gets so a Redis outage doesn't stall
upstream fan-out, and a retry on Sets. Work with my existing eRPC config. Read the
reference first: https://docs.erpc.cloud/config/database/drivers.llms.txt
Prompt Example #2: layer a read-only gRPC BDS connector for historical data
I want to add a read-only gRPC BDS connector for historical EVM chain data in
front of my existing Redis connector — so finalized block responses are served from
the BDS layer and only cache misses go to live upstreams. Add failsafe policies
(hedge + timeout + retry) on Gets so a slow gRPC call doesn't block requests. My
Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/database/drivers.llms.txt
Prompt Example #3: tune memory connector sizing for my workload
My eRPC instance uses the in-process memory connector. I'm seeing high eviction
rates in Grafana and want to resize maxItems and maxTotalSize correctly for 8 GB of
RAM and a workload of ~2 million cached EVM responses. Also enable emitMetrics so I
can track ristretto cost going forward. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/database/drivers.llms.txt
Prompt Example #4: debug DynamoDB lock contention causing high request volume
My eRPC DynamoDB connector is causing a spike of DynamoDB requests under load —
I think the lockRetryInterval is zero and the retry loop is spinning. Audit my
my eRPC config DynamoDB connector config, fix the lock retry interval, and explain
the other DynamoDB footguns I should watch for (TTL lag, accessKeyID casing, etc.).
Reference: https://docs.erpc.cloud/config/database/drivers.llms.txt
Prompt Example #5: migrate from memory to PostgreSQL without downtime
I currently have a single in-process memory connector in my eRPC config and want
to migrate to PostgreSQL for persistence. Show me the full connector config with
correct timeout values (not from the URI), connection pool sizing, and a pg_cron
cleanup setup. Explain what changes and what behavior to expect. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/database/drivers.llms.txt
Prompt Example #6: use AWS IAM Auth with ElastiCache / RDS connectors
I run eRPC on EKS with an IRSA role and want to use IAM Auth instead of static passwords
for my Redis (ElastiCache) and PostgreSQL (RDS) cache connectors.
Configure connectors to use iamAuth, derive region/endpoint/dbUser where possible,
omit the auth block so the instance role is used, and tell me the exact IAM policy
actions and the rds_iam SQL grant I need. Explain the token-rotation behavior and the
12-hour ElastiCache disconnect handling. Work with my existing eRPC config. Reference:
https://docs.erpc.cloud/config/database/drivers.llms.txt
Storage drivers — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How it works

NewConnector reads cfg.Driver, instantiates the matching back-end, then optionally wraps it in FailsafeConnector when failsafeForGets or failsafeForSets is non-empty. The Connector interface exposes Get, Set, Delete, List, Lock, WatchCounterInt64, and PublishCounterInt64. All five drivers implement every method — except List on memory (intentionally unimplemented) and all write methods on gRPC (read-only).

Two index constants control lookup paths: ConnectorMainIndex ("idx_main") for direct key lookups and ConnectorReverseIndex ("idx_reverse") for wildcard range-key resolution. Set always writes the main index and, for evm:-prefixed partition keys, also writes a reverse index entry so wildcard gets resolve without full scans.

Key encoding. The composite storage key is <partitionKey>:<rangeKey>. For EVM cache entries the partition key typically starts with evm: (e.g., evm:1:0x12345678). Reverse index entries for memory and Redis use the key format rvi#<wildcardPartitionKey>#<rangeKey> where wildcardPartitionKey is evm:<chainId>:*. PostgreSQL and DynamoDB store a dedicated reverse-index row or GSI entry instead.

Distributed counter payload. WatchCounterInt64 and PublishCounterInt64 exchange a JSON-serialized CounterInt64State struct: {"v": 42, "t": 1718000000000, "b": "pod-name"} where v is the counter value, t is unix milliseconds (t ≤ 0 means uninitialized), and b is the best-effort reporter identity (hostname/pod). — data/connector.go:L34-L38 (opens in a new tab)

Memory connector (ristretto). The memory connector uses dgraph-io/ristretto v2 (opens in a new tab) as an in-process LRU with cost-based eviction. Ristretto is initialized with NumCounters = 3 × maxItems, MaxCost = maxTotalSizeBytes, and BufferItems = 64. Each entry costs len(value) + 256 bytes against maxTotalSize. TTL is enforced by ristretto internally — no background cleanup goroutine. Reverse index entries for EVM keys are stored without TTL; after the main entry expires the stale pointer resolves to a miss. List is not implemented; ristretto has no iteration API. Lock uses a sync.Map of *sync.Mutex with TryLock plus a backoff loop (starts at 2ms, increments by 1ms per attempt, caps at 20ms). WatchCounterInt64 and PublishCounterInt64 are no-ops.

Redis connector. Redis is the recommended connector for multi-instance deployments. It supports List (cursor-based SCAN — main index scans all keys with *; reverse index scans with rvi#* prefix, values fetched in pipeline), distributed locking via go-redsync (opens in a new tab), and pub/sub through a self-healing RedisPubSubManager (channel name: counter:<key>). Reverse index entries inherit the same TTL as the main entry. On reverse-index Get, the connector verifies the resolved key's TTL: -2s means the key does not exist → ErrRecordNotFound; -1s means persistent (accepted); positive value means it has an active TTL (accepted). Connection setup is non-blocking: NewRedisConnector enqueues a bootstrap task and returns immediately. Reconnection fires only on a narrow allowlist of critical failure strings ("connection refused", "broken pipe", "invalid connection", "connection reset by peer", "no such host", "network is unreachable", "connection closed") to avoid spurious reconnects. When rediss:// URI and tls.enabled: true are both configured, YAML cert/CA overrides merge onto the URI-derived TLS baseline and disable InsecureSkipVerify. A rediss://-only URI without a YAML tls: block produces InsecureSkipVerify=true.

PostgreSQL connector. PostgreSQL stores cache entries with columns (partition_key, range_key, value BYTEA, expires_at TIMESTAMPTZ). Schema migration (including a TEXT -> BYTEA column migration), indexes, and an optional pg_cron cleanup job run once per process lifetime — reconnects do not re-run them. Connection pool settings are hardcoded at MaxConnLifetime = 5h and MaxConnIdleTime = 30m. The old pool is closed only after the lock is released during reconnect, preventing hot-path serialization. Wildcard Get translates * to % and uses SQL LIKE: for ConnectorReverseIndex the query is WHERE range_key = $1 AND partition_key LIKE $2; for main index: WHERE partition_key = $1 AND range_key LIKE $2, returning the first row ordered by partition_key DESC. Distributed locking uses pg_try_advisory_xact_lock(hash64(key)) (FNV-64a hashes the key to an int64 lock ID), which is non-blocking: if the lock is held the call returns immediately with an error. TTL enforcement is exact via server-side expires_at filtering; pg_cron or a local ticker deletes expired rows every 5 minutes. WatchCounterInt64 uses PostgreSQL LISTEN/NOTIFY (pub/sub), falling back to 30-second polling if LISTEN is unavailable. Channel names are sanitized to valid PostgreSQL identifier characters (max 63 bytes).

DynamoDB connector. DynamoDB uses separate read (2048 max idle connections) and write (256 max idle connections) HTTP/2 clients. The table is created with PAY_PER_REQUEST billing if absent. TTL is stored as a numeric unix epoch attribute; AWS native TTL expiry is eventually consistent and can lag up to ~48 hours. The connector guards with a client-side epoch comparison on every Get, returning ErrRecordExpired for items past TTL. For reverse-index Query, up to 10 items are fetched with a server-side FilterExpression and the first non-expired item is chosen client-side. Both B (binary) and S (string) value attribute types are read for backward compatibility — legacy string values are returned as []byte. WatchCounterInt64 uses periodic polling (5s default) — DynamoDB has no native pub/sub. PublishCounterInt64 is a no-op; callers rely on polling to pick up state changes.

gRPC connector (read-only). The gRPC connector is a read-through layer backed by BDS (Blockchain Data Standards) gRPC servers that hold historical EVM chain data. It supports no writes. Configure it alongside a write-capable connector in a multi-connector cache policy. Server discovery supports both a static servers list and a bootstrap HTTP URL. Only a specific allowlist of methods is forwarded to BDS: eth_getBlockByNumber, eth_getBlockByHash, eth_getLogs, eth_getTransactionByHash, eth_getTransactionReceipt, eth_getBlockReceipts, eth_chainId, and eth_blockNumber; unsupported methods return a fast miss ((nil, nil)) without an RPC call. Fast-miss rejection: if the request block number is below earliestByNetwork[networkId] (and that value is > 0), ErrRecordNotFound is returned immediately without an RPC. A background goroutine polls chain head every 60 seconds, implementing the CacheHeadReporter interface used by the real-time cache freshness gate. eth_blockNumber has no native BDS method; the connector intercepts it, calls eth_getBlockByNumber("latest", false) internally, and returns only the number field as a canonical hex string with leading zeros normalized via uint64 round-trip (zero block → "0x0"). On failure it returns ErrRecordNotFound so the request falls through to live upstreams.

FailsafeConnector. Any connector can be wrapped by setting failsafeForGets and/or failsafeForSets. Each entry specifies a matchMethod pattern and optional matchFinality list, plus one or more of retry, circuit-breaker, hedge, and timeout policies. Executor selection (pickCacheExecutor) reads the method and finality from ctx.Value(common.RequestContextKey); if no request is attached (background prefetch, tests), method = "" and finality = 0. The most-specific match wins: (method + finality) > (method only) > (finality only) > wildcard. A no-op executor is always appended so unmatched operations proceed unconditionally. List, Lock, WatchCounterInt64, and PublishCounterInt64 bypass all failsafe policies. Retry fires only on transport errors — cache misses, expired records, and context cancellation are never retried. Transport errors recognized by isTransportError include net.Error timeouts, io.EOF/ErrUnexpectedEOF, syscall connection errors, gRPC status codes Unavailable/DeadlineExceeded/Aborted, Redis cluster transients (CLUSTERDOWN, MASTERDOWN, TRYAGAIN, LOADING), HTTP/2 GOAWAY, and "use of closed network connection". Hedge supports only static delays at the connector layer; quantile-based delays are rejected at construction time.

AWS IAM authentication (Redis & PostgreSQL). Both the Redis (ElastiCache) and PostgreSQL (RDS) connectors can authenticate with short-lived AWS IAM tokens instead of static passwords. A shared createAWSSession helper resolves credentials from iamAuth.auth (same modes as dynamodb.auth) or, when omitted, the AWS SDK default chain (instance role → IRSA → env → shared file). For ElastiCache, eRPC presigns a SigV4 token (via aws/signer/v4, scheme stripped) and feeds it through go-redis's CredentialsProviderContext, which fires on every new physical connection; ConnMaxLifetime is pinned to 11h (±30m jitter) so each connection refreshes its token well before AWS's 12-hour forced disconnect — no background goroutines. For RDS, eRPC calls rdsutils.BuildAuthToken inside pgxpool's BeforeConnect hook, minting a fresh token per new pool connection; tokens are valid 15 minutes but only checked at connect time, and RDS has no 12-hour cap so the 5h MaxConnLifetime is unchanged. IAM auth is also available for rateLimiters.store.redis: set iamAuth.enabled: true on the store.redis block and eRPC builds a radix/v3 pool whose PoolConnFunc mints a fresh SigV4 token on every new physical connection; PoolMaxLifetime is pinned to 11h so connections rotate before AWS's 12-hour forced disconnect (radix lacks a jitter knob — connections spread naturally across the pool's dial history).

Config schema

Top-level ConnectorConfigcommon/config.go:L341-352

FieldTypeDefaultBehavior / footguns
idstring"<scope>-<driver>" e.g. "cache-memory"common/defaults.go:L847-848Unique identifier; appears as the connector label on all metrics.
driverstringInferred from whichever sub-config is present; explicit driver wins when both are set — common/defaults.go:L876-930One of: memory, redis, postgresql, dynamodb, grpc. Unknown value → ErrInvalidConnectorDriver at startup.
failsafeForGets[]*FailsafeConfignilPolicies applied to Get. Evaluated by pickCacheExecutor in declaration order.
failsafeForSets[]*FailsafeConfignilPolicies applied to Set and Delete.

Each FailsafeConfig entry under failsafeForGets / failsafeForSets:

FieldTypeDefaultNotes
matchMethodstring"*"common/defaults.go:L855-857Glob pattern matched against the request method. "*" = all methods.
matchFinality[]DataFinalityStatenil (any)Filter by finality; nil matches any finality.
retry.maxAttemptsintRetries only on isTransportError.
retry.delayDurationBase delay between attempts.
retry.backoffMaxDelayDurationMax delay with exponential backoff.
retry.backoffFactorfloat64Backoff multiplier per attempt.
circuitBreaker.*Standard breaker fields. ErrRecordNotFound and ErrRecordExpired count as OutcomeIgnore, not failures.
timeout.durationDurationPer-operation timeout via context.WithTimeoutCause.
hedge.delayDurationStatic delay only. Quantile-based hedge is rejected at construction: "hedge quantile is not supported for connector-level failsafe"data/cache_executor.go:L37-41
hedge.maxCountintMax additional hedged copies.
consensusnilRejected at construction with "consensus is not supported for connector-level failsafe"data/cache_executor.go:L31-35

Memory connector — common/config.go:L361-365, defaults common/defaults.go:L935-943

FieldTypeDefaultBehavior / footguns
memory.maxItemsint100000Sets NumCounters = 3 × maxItems (ristretto frequency sketch). Must be > 0 or init fails.
memory.maxTotalSizestring"1GB"Parsed by go-humanize; becomes ristretto MaxCost in bytes. Must be > 0.
memory.emitMetrics*boolnil (disabled)When true, background goroutine emits ristretto metrics every 30s — data/memory.go:L212-294

Redis connector — common/config.go:L383-395, defaults common/defaults.go:L946-1018

FieldTypeDefaultBehavior / footguns
redis.uristringRedis URI (redis:// or rediss://). Mutually exclusive with addr.
redis.addrstringhost:port; converted to URI at defaults time then cleared.
redis.usernamestringFolded into URI; cleared afterward.
redis.passwordstringRedacted in logs/JSON. Folded into URI; cleared afterward.
redis.dbint0Database index appended as /<db> in URI.
redis.tls.enabledboolfalseUses rediss:// scheme; merges with URI-inferred TLS if both present.
redis.tls.certFilestringClient certificate for mTLS.
redis.tls.keyFilestringClient private key for mTLS.
redis.tls.caFilestringCustom CA bundle for server cert verification.
redis.tls.insecureSkipVerifyboolfalseSkip server certificate verification.
redis.connPoolSizeint8go-redis connection pool size.
redis.initTimeoutDuration5sPing timeout per connection attempt. Falls back to DialTimeout from URI when zero.
redis.getTimeoutDuration1sPer-Get deadline. Falls back to ReadTimeout from URI when zero.
redis.setTimeoutDuration3sPer-Set/Delete deadline. Falls back to WriteTimeout from URI when zero.
redis.lockRetryIntervalDuration500msSleep between redsync mutex acquisition retries.
redis.iamAuth.enabledboolfalseEnables AWS ElastiCache IAM auth. SetDefaults auto-enables TLS (promotes addr to rediss://); the connector sets ConnMaxLifetime=11h (±30m jitter) so connections rotate before AWS's 12h forced disconnect. Mutually exclusive with a static password — rejected at startup. — data/redis_iam_auth.go:L22-24, wiring data/redis.go:L187-201
redis.iamAuth.cacheNamestringElastiCache replication-group ID. Required when enabled. Lowercased automatically (AWS lowercases cache names at creation).
redis.iamAuth.userIDstringElastiCache user. Required when enabled. The user name and user ID must be identical — supply that single value.
redis.iamAuth.regionstringderivedAWS region; derived from AWS_REGION / instance metadata when omitted.
redis.iamAuth.auth.**AwsAuthConfignil (default chain)Optional AWS credential source for signing the token (same fields/casing as dynamodb.auth). Omit to use the instance-role / IRSA default chain (recommended). — common/config.go:L518-529

ElastiCache Serverless is not supported (it lacks PSUBSCRIBE, required by eRPC's shared-state connector). Requires Valkey ≥ 7.2 or Redis OSS ≥ 7.0 with IAM auth and in-transit TLS enabled on the cache.

PostgreSQL connector — common/config.go:L445-453, defaults common/defaults.go:L1021-1058

FieldTypeDefaultBehavior / footguns
postgresql.connectionUristringpgx connection string (required). Redacted in logs.
postgresql.tablestring"erpc_json_rpc_cache" (cache), "erpc_shared_state" (shared-state), "erpc_auth" (auth)Table created on first connect.
postgresql.minConnsint324 (non-auth), 1 (auth)pgxpool MinConns.
postgresql.maxConnsint3232 (non-auth), 4 (auth)pgxpool MaxConns. Pool lifecycle: MaxConnLifetime=5h, MaxConnIdleTime=30m.
postgresql.initTimeoutDuration5sContext deadline for pool creation. NOT read from URI — always explicit.
postgresql.getTimeoutDuration1sPer-Get/List query deadline. Never falls back to URI values.
postgresql.setTimeoutDuration2sPer-Set/Delete query deadline. Never falls back to URI values.
postgresql.iamAuth.enabledboolfalseEnables AWS RDS IAM auth. Mints a fresh SigV4 token per new pool connection via pgx BeforeConnect; SetDefaults appends sslmode=require automatically. connectionUri must carry no password. RDS has no 12h cap, so MaxConnLifetime (5h) is unchanged. — data/postgresql_iam_auth.go:L21-38, wiring data/postgresql.go:L186-194
postgresql.iamAuth.regionstringderivedAWS region; derived from AWS_REGION / instance metadata when omitted.
postgresql.iamAuth.endpointstringderived from connectionUrihost:port used to sign the token. Must include a port. — common/defaults.go:L1071-1090
postgresql.iamAuth.dbUserstringderived from connectionUriDB user the token authenticates as (must be granted rds_iam). When both are set it must match the connectionUri user, else startup fails with a mismatch error.
postgresql.iamAuth.auth.**AwsAuthConfignil (default chain)Optional AWS credential source for signing. Omit to use the default chain. — common/config.go:L534-545

DynamoDB connector — common/config.go:L428-443, defaults common/defaults.go:L1061-1099

FieldTypeDefaultBehavior / footguns
dynamodb.tablestring"erpc_json_rpc_cache" / "erpc_shared_state" / "erpc_auth"Created with PAY_PER_REQUEST if absent.
dynamodb.regionstringAWS region (required; fatal if empty).
dynamodb.endpointstringCustom endpoint (e.g., DynamoDB Local).
dynamodb.auth.modestring"file" / "env" / "secret". Nil auth uses default credential chain.
dynamodb.auth.credentialsFilestringPath to AWS credentials file (mode=file).
dynamodb.auth.profilestringAWS profile name.
dynamodb.auth.accessKeyIDstringCase-sensitive: struct tag is yaml:"accessKeyID" (capital I and D). Using accessKeyId (lowercase d) silently skips the field. — common/config.go:L483
dynamodb.auth.secretAccessKeystringStatic secret key (mode=secret).
dynamodb.partitionKeyNamestring"groupKey"DynamoDB partition key attribute name.
dynamodb.rangeKeyNamestring"requestKey"DynamoDB sort key attribute name.
dynamodb.reverseIndexNamestring"idx_requestKey_groupKey"GSI name for reverse lookups. Added if absent.
dynamodb.ttlAttributeNamestring"ttl"Numeric attribute holding unix epoch seconds for TTL.
dynamodb.initTimeoutDuration5sSession creation and table/index init timeout.
dynamodb.getTimeoutDuration1sPer-Get deadline.
dynamodb.setTimeoutDuration2sPer-Set/Delete/Lock deadline.
dynamodb.maxRetriesint0 (SDK default)AWS SDK retry count for transient errors.
dynamodb.statePollIntervalDuration5sWatchCounterInt64 polling interval.
dynamodb.lockRetryIntervalDurationno default (zero)Footgun: zero duration → retry loop spins as fast as the API under lock contention. Always set to 100ms in production. — data/dynamodb.go:L660-688

gRPC connector — common/config.go:L354-359, defaults common/defaults.go:L927-929

FieldTypeDefaultBehavior / footguns
grpc.bootstrapstringHTTP URL returning JSON array or newline-separated gRPC server URLs. Fetched once at startup; failure → connector init error.
grpc.servers[]stringExplicit server list. Combined with bootstrap results when both present.
grpc.headersmap[string]stringHeaders sent on every gRPC call.
grpc.getTimeoutDuration100msPer-Get call timeout. Applied only when shorter than existing context deadline.

Timeout buffer constants — data/timeout_constants.go:L7-15

ConstantValueUsage
DefaultOperationBuffer10sTime reserved after lock acquisition for in-lock operations. Used by Redis lock retry budget calculation.
PollOperationBuffer15sReserved in the state poller after lock acquisition.
MinPollTimeout30sMinimum timeout for a complete poll cycle.

Worked examples

All patterns below are distilled from real production fleets; comments explain the non-obvious choices.

1. Split memory caches for unfinalized vs. finalized data. Production deployments keep two separate in-process connectors — a large one for the high-volume unfinalized stream (recently mined blocks, txs still propagating) and a smaller one for immutable finalized data. This lets ristretto tune its frequency sketch and cost budget independently for each access pattern:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: memory-cache-unfinalized        driver: memory        memory:          # high-traffic: recent blocks, pending txs, unconfirmed receipts          maxItems: 2000000          # NumCounters = 3 × maxItems = 6 M — enough sketch space for 2 M hot keys          maxTotalSize: "8GB"      - id: memory-cache-finalized        driver: memory        memory:          # lower traffic: immutable block/tx data older than ~128 blocks          maxItems: 500000          maxTotalSize: "4GB"

2. gRPC BDS connector with hedge + timeout failsafe. The canonical production pattern: a gRPC historical-data reader as the first connector, wrapped with a 400ms timeout and a 100ms static hedge so a slow BDS server fires a backup rather than stalling the request. Quantile hedge is rejected at this layer — static delay only:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: bds-cache        driver: grpc        grpc:          # static server list — or use bootstrap: URL for dynamic discovery          servers:            - grpc://bds-reader.example.com:50051          # cap total gRPC call time; past this, go to live upstreams instead          getTimeout: 400ms        failsafeForGets:          - matchMethod: "*"            retry:              maxAttempts: 2              # retry immediately — a slow BDS call beats a fast empty one              delay: "0"            timeout:              # 400ms = the same as getTimeout; belt-and-suspenders at the              # failsafe layer in case gRPC context propagation is delayed              duration: 400ms            hedge:              # static delay only — quantile hedge is rejected for connectors              delay: 100ms              maxCount: 1

3. gRPC connector with bearer-authenticated edge API and a Redis write layer. The gRPC connector is read-only; pair it with Redis for writes. Production configs using a public BDS edge gateway attach auth headers per-connector — secrets stay in environment variables, not in the config file:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: bds-edge        driver: grpc        grpc:          servers:            - grpc://bds-edge-api.example.com:443          getTimeout: 400ms          headers:            # bearer token loaded from env — never hardcode secrets in config            Authorization: "Bearer ${BDS_API_TOKEN}"        failsafeForGets:          - matchMethod: "*"            retry: { maxAttempts: 2, delay: "0" }            timeout: { duration: 400ms }            hedge: { maxCount: 1, delay: 100ms }      - id: redis-write        driver: redis        redis:          uri: "${REDIS_URL}"          connPoolSize: 16

4. Redis shared state for multi-pod deployments. When multiple eRPC instances need to coordinate (shared scoring state, distributed locking), use Redis for sharedState. This is separate from the cache connector — a single Redis URI with no extra config needed:

database.sharedState
erpc.yaml
database:  sharedState:    # must be unique across all eRPC clusters sharing this Redis    clusterKey: my-erpc-prod    connector:      driver: redis      redis:        # credentials via env var — never inline in config        uri: "${SHARED_STATE_REDIS_URL}"

5. DynamoDB for serverless deployments. Useful when you want fully managed persistence without operating Redis. Set lockRetryInterval explicitly — the default is zero and the retry loop spins as fast as the DynamoDB API under contention, which can exhaust provisioned throughput:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: ddb-cache        driver: dynamodb        dynamodb:          region: us-east-1          table: erpc_json_rpc_cache          # CRITICAL: no default — zero = spin loop under lock contention          lockRetryInterval: 100ms          getTimeout: 1s          setTimeout: 2s          auth:            mode: env   # or "file" / "secret"            # YAML key is accessKeyID (capital I and D) — accessKeyId silently fails            accessKeyID: "${AWS_ACCESS_KEY_ID}"            secretAccessKey: "${AWS_SECRET_ACCESS_KEY}"

6. Passwordless AWS IAM auth (ElastiCache + RDS). Run on EC2/EKS with an instance role (or IRSA) and drop static passwords entirely. Omit iamAuth.auth so the AWS SDK default chain provides credentials; eRPC mints and rotates SigV4 tokens automatically. Note there is no password in either connection string, and region/endpoint/dbUser are derived when omitted:

database.evmJsonRpcCache.connectors[]
erpc.yaml
database:  evmJsonRpcCache:    connectors:      - id: elasticache-iam        driver: redis        redis:          # no password — IAM token is injected as AUTH on every new connection          addr: my-cluster.abc123.ng.0001.use1.cache.amazonaws.com:6379          iamAuth:            enabled: true            cacheName: my-cluster        # replication-group ID (auto-lowercased)            userID: iam-user-01          # ElastiCache user name == user ID            # region derived from AWS_REGION / instance metadata; auth omitted = instance role          # TLS is auto-enabled (rediss://); add a tls block only for a custom CA      - id: rds-iam        driver: postgresql        postgresql:          # no password in the URI; sslmode=require is appended automatically          connectionUri: postgres://erpc-user@mydb.abc123.us-east-1.rds.amazonaws.com:5432/erpc          table: erpc_json_rpc_cache          iamAuth:            enabled: true            # region, endpoint and dbUser are derived from connectionUri / metadata

The RDS database user must exist and hold rds_iam before first connect:

CREATE USER "erpc-user" WITH LOGIN;
GRANT rds_iam TO "erpc-user";
 
-- eRPC creates the table and indexes on first connect; the user needs CREATE on
-- the schema. PostgreSQL ≥15 no longer grants public-schema CREATE by default:
GRANT ALL ON SCHEMA public TO "erpc-user";

Minimal IAM policy for the instance role:

[
  {
    "Effect": "Allow",
    "Action": "elasticache:Connect",
    "Resource": [
      "arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-cluster",
      "arn:aws:elasticache:us-east-1:123456789012:user:iam-user-01"
    ]
  },
  {
    "Effect": "Allow",
    "Action": "rds-db:connect",
    "Resource": "arn:aws:rds-db:us-east-1:123456789012:dbuser:db-XXXXXXXXXXXXXXXX/erpc-user"
  }
]

The RDS db-XXXXXXXXXXXXXXXX resource ID is the instance's resource ID (RDS console → instance details), not its name.

Request/response behavior

  • NewConnector dispatches on cfg.Driver. Unknown driver → ErrInvalidConnectorDriver at startup; never returned during live request processing. [data/connector.go:L63-103]
  • Set always writes ConnectorMainIndex and, for evm:-prefixed partition keys, also writes a reverse index entry. Memory reverse index entries carry no TTL; stale pointers after main entry expiry resolve to cache misses. [data/memory.go:L124-131]
  • DynamoDB applies a client-side epoch check on every Get and returns ErrRecordExpired for items past TTL regardless of AWS purge lag. Callers must distinguish ErrRecordExpired from ErrRecordNotFound. [data/dynamodb.go:L547-556]
  • gRPC intercepts eth_blockNumber and translates it to eth_getBlockByNumber("latest", false), returning only the number field as a canonical hex string. On failure returns ErrRecordNotFound (falls through to real upstreams). [data/grpc.go:L265-278]
  • FailsafeConnector bypasses failsafe policies for List, Lock, WatchCounterInt64, and PublishCounterInt64. Only Get, Set, and Delete get failsafe treatment. [data/failsafe.go:L308-322]
  • FailsafeConnector forwards CacheLatestBlockTimestamp to the wrapped connector so the freshness gate works even when the gRPC connector is wrapped. [data/failsafe.go:L211-216]
  • isTransportError is the sole retryability gate: ErrRecordNotFound, ErrRecordExpired, context.Canceled, and context.DeadlineExceeded are not transport errors and are never retried. [data/failsafe.go:L25-92]
  • pickCacheExecutor four-tier priority: (method + finality) > (method only) > (finality only) > (wildcard). A no-op executor is always appended so unmatched operations proceed unconditionally. [data/failsafe.go:L159-201]

Best practices

  • Use Redis for multi-instance deployments — memory is single-process only and List is unimplemented; any shared-state or admin endpoint that needs List silently breaks with memory.
  • Set dynamodb.lockRetryInterval: 100ms explicitly in production. DynamoDB has no default; zero means the retry loop spins as fast as the HTTP API responds under contention. Redis defaults to 500ms.
  • Set PostgreSQL timeouts explicitly; do not rely on the connection URI. Unlike Redis, initTimeout, getTimeout, and setTimeout are always Go context deadlines and are never read from connectionUri. Setting connect_timeout=5 in the DSN has no effect on these fields.
  • Always supply a caFile or client cert when using rediss:// in production. A rediss://-only URI without a YAML tls: block produces InsecureSkipVerify=true.
  • Never use dynamodb.auth.accessKeyId (lowercase d). The struct tag is yaml:"accessKeyID" — a lowercase d silently skips the field and falls through to the default credential chain.
  • Plan for a 60-second warm-up when using the gRPC connector with the freshness gate. The block-head poller first fires at T+60s. During this window CacheLatestBlockTimestamp returns ok=false and the freshness gate fails open — stale realtime responses may be served.
  • Wrap the gRPC connector with a circuit-breaker in failsafeForGets to avoid hammering a BDS server that is returning errors; without it, every cache request makes a gRPC call when the BDS layer is unhealthy.
  • Prefer IAM auth over static passwords on AWS. With redis.iamAuth / postgresql.iamAuth enabled and iamAuth.auth omitted, the instance role / IRSA chain provides credentials and eRPC rotates short-lived tokens automatically — no secrets in config, no rotation tooling. Keep host clocks NTP-synced; SigV4 tokens are time-bound (±5 minutes).

Edge cases & gotchas

  1. Memory List is not implemented. Returns an error with guidance to use Redis/PostgreSQL/DynamoDB. Any subsystem needing List (admin endpoints, shared-state enumeration) must not use the memory driver. data/memory.go:L316-L321 (opens in a new tab)

  2. Memory reverse index has no TTL. Main entries expire; their reverse index pointers remain until cost-based eviction. Stale pointers resolve to misses — no incorrect data is served. Redis correctly propagates TTL to reverse index entries. data/memory.go:L128-L130 (opens in a new tab)

  3. Memory cost overhead is 256 bytes per entry. Even a 1-byte value costs 257 bytes against maxTotalSize. Small-value workloads require a proportionally larger maxTotalSize. data/memory.go:L78-L80 (opens in a new tab)

  4. DynamoDB TTL lag (~48h). AWS native TTL expiry is eventually consistent. The connector applies a client-side epoch check returning ErrRecordExpired, but callers must distinguish ErrRecordExpired from ErrRecordNotFound since the expired item still exists in DynamoDB. data/dynamodb.go:L547-L556 (opens in a new tab)

  5. DynamoDB lockRetryInterval has no default. Zero duration → retry loop spins as fast as the DynamoDB API under contention. Set dynamodb.lockRetryInterval: 100ms in production. Contrast: Redis defaults to 500ms. data/dynamodb.go:L660-L688 (opens in a new tab)

  6. PostgreSQL advisory lock is non-blocking. pg_try_advisory_xact_lock returns immediately with false if locked. No built-in retry loop — callers must implement their own. data/postgresql.go:L558-L573 (opens in a new tab)

  7. PostgreSQL timeouts cannot be set via connection URI. Unlike Redis, initTimeout, getTimeout, and setTimeout are always Go context deadlines. Setting connect_timeout=5 in the DSN does NOT populate initTimeout. Configure all three explicitly in YAML. data/postgresql.go:L130-L134 (opens in a new tab)

  8. PostgreSQL cron.schedule is not idempotent. applySchema is gated by a schemaApplied flag (set once per process lifetime) to prevent duplicate pg_cron jobs on reconnect. data/postgresql.go:L257-L268 (opens in a new tab)

  9. gRPC connector supports no writes. If configured as the sole connector for a write-through cache policy, all writes silently fail. Use it only as a read-through layer alongside a write-capable connector. data/grpc.go:L454-L478 (opens in a new tab)

  10. gRPC head poller first fires at T+60s. Block-head gauges are zero for the first 60 seconds. During this window the freshness gate fails open — cached realtime responses may be served. data/grpc.go:L302-L313 (opens in a new tab)

  11. gRPC bootstrap fetch is not retried. If fetchGrpcServers fails at startup, NewGrpcConnector returns an error. Individual per-server connection tasks ARE retried by the initializer. data/grpc.go:L88-L95 (opens in a new tab)

  12. gRPC freshness gate fails open for non-gRPC connectors. Memory, Redis, PostgreSQL, and DynamoDB do not implement CacheHeadReporter. When eth_blockNumber or eth_gasPrice is fetched from one of these connectors, shouldAcceptCachedResult cannot determine response age and unconditionally accepts the cached result. architecture/evm/json_rpc_cache.go:L862-L920 (opens in a new tab)

  13. gRPC block-head poller: zero latestTs means "unknown", not genesis block. When eth_getBlockByNumber("latest") returns a block with a zero or absent timestamp field, CacheLatestBlockTimestamp returns ok=false and the freshness gate fails open. data/grpc.go:L349-L357 (opens in a new tab)

  14. Hedge quantile rejected for connector-level failsafe. Only static delay.base / delay.min work. hedge.delay.quantile > 0 fails validation with "hedge quantile is not supported for connector-level failsafe". data/cache_executor.go:L37-L41 (opens in a new tab)

  15. Consensus rejected for connector-level failsafe. consensus non-nil at construction → ErrFailsafeConfiguration at startup. data/cache_executor.go:L31-L35 (opens in a new tab)

  16. dynamodb.auth.accessKeyID is case-sensitive. The struct tag is yaml:"accessKeyID" (capital I and D). Using accessKeyId (lowercase d) silently skips the field; AWS falls through to the default credential chain. common/config.go:L483 (opens in a new tab)

  17. Redis TLS merging with rediss:// URIs. When both a rediss:// URI and tls.enabled: true are configured, YAML cert/CA overrides merge onto the URI-derived TLS baseline and disable InsecureSkipVerify. A rediss://-only URI without a YAML tls: block produces InsecureSkipVerify=true. Always supply a caFile or client cert when using rediss:// in production. data/redis.go:L156-L180 (opens in a new tab)

  18. FailsafeConnector wrapping does not break CacheHeadReporter. FailsafeConnector forwards CacheLatestBlockTimestamp to the wrapped connector, so the freshness gate works correctly even when the gRPC connector has failsafe policies. data/failsafe.go:L211-L216 (opens in a new tab)

  19. gRPC freshness gate: response timestamp takes precedence over connector head. shouldAcceptCachedResult first extracts a block timestamp from the cached response itself. Only if the response carries no timestamp does it fall back to CacheLatestBlockTimestamp. For eth_getBlockByNumber (which returns a full block with a timestamp), the response timestamp is used regardless of the connector's polled head. architecture/evm/json_rpc_cache.go:L862-L920 (opens in a new tab)

  20. PostgreSQL pub/sub falls back to 30-second polling. If LISTEN/NOTIFY is unavailable (e.g., connection pooler that strips it), WatchCounterInt64 falls back to a 30-second polling interval. Channel names are sanitized to valid PostgreSQL identifier characters with a maximum of 63 bytes. data/postgresql.go:L1103-L1127 (opens in a new tab)

  21. DynamoDB backward compatibility: S and B attribute types both accepted. When reading items, both binary (B) and legacy string (S) value attributes are handled. Legacy string values are returned as []byte. If neither attribute is present, the item is skipped. data/dynamodb.go:L512-L566 (opens in a new tab)

  22. PostgreSQL pool lock timing. During reconnect, connectTask holds the write lock only for the field swap (nanoseconds). Schema setup and pool creation happen outside the lock. The old pool is closed after releasing the lock to prevent blocking hot-path reads/writes during pool drain. data/postgresql.go:L176-L250 (opens in a new tab)

  23. IAM auth + static password is rejected at startup. Redis rejects a password embedded in the URI when iamAuth.enabled: true; PostgreSQL rejects a password in connectionUri. Remove the password — the IAM token is injected at connect time. common/validation.go:L556-L630 (opens in a new tab)

  24. ElastiCache userID must equal the user name. For IAM-enabled ElastiCache users the user name and user ID are identical by AWS requirement; supply that single value as userID. A mismatch authenticates as the wrong user or fails. cacheName is lowercased automatically (AWS lowercases at creation), but the ElastiCache user must still match userID exactly. common/defaults.go:L971-L982 (opens in a new tab)

  25. ElastiCache IAM connections are force-disconnected at 12h. AWS drops IAM-authenticated connections after 12 hours. eRPC pins ConnMaxLifetime=11h (±30m jitter) so go-redis rotates connections — re-invoking CredentialsProviderContext for a fresh token — before the cliff. Do not override the pool lifetime when IAM auth is on. ElastiCache Serverless is unsupported (no PSUBSCRIBE). data/redis_iam_auth.go:L22-L24 (opens in a new tab)

  26. RDS iamAuth.dbUser must match the connectionUri user. When both are set and differ, startup fails — pgx would otherwise sign the token for dbUser while connecting as the URI user, a silent auth mismatch. The user must hold rds_iam and SSL is mandatory (sslmode=require is appended automatically; an explicit weaker sslmode is rejected). common/validation.go:L556-L590 (opens in a new tab)

  27. SigV4 tokens are clock-sensitive. ElastiCache presigned tokens and RDS auth tokens are time-bound (±5 minutes). Unsynced host clocks (no NTP) cause intermittent auth failures on new connections. data/redis_iam_auth.go:L1-L86 (opens in a new tab)

Observability

Prometheus metrics — telemetry/metrics.go:L73-107, telemetry/metrics.go:L628-638

MetricTypeLabelsWhen it fires
erpc_ristretto_cache_current_costgaugeconnectorEvery 30s by memory connector when emitMetrics=true. Bytes currently used by ristretto.
erpc_ristretto_cache_sets_failed_totalcounterconnectorEvery 30s; delta of SetsDropped + SetsRejected from ristretto stats.
erpc_cache_connector_earliest_block_numbergaugeconnector, networkSet every 60s when gRPC fetchTaggedBlock("earliest") succeeds. Drives fast-miss rejection. Not reset on transient poll failure.
erpc_cache_connector_latest_block_numbergaugeconnector, networkSet every 60s when gRPC fetchTaggedBlock("latest") succeeds.
erpc_cache_connector_finalized_block_numbergaugeconnector, networkSet every 60s when finalized block is fetchable.
erpc_cache_connector_earliest_block_timestamp_secondsgaugeconnector, networkUnix timestamp (seconds) of the earliest known block. Set only when block timestamp > 0.
erpc_cache_connector_latest_block_timestamp_secondsgaugeconnector, networkUnix timestamp of the latest block. Drives the freshness gate via CacheLatestBlockTimestamp. Alert when this lags wall-clock by more than your expected block interval. Zero means unknown → freshness gate fails open.
erpc_cache_connector_finalized_block_timestamp_secondsgaugeconnector, networkUnix timestamp of the finalized block. Set only when block timestamp > 0.

Trace spans

Span nameConnectorNotable attributes
RedisConnector.SetRedispartition_key, range_key, value_size
RedisConnector.GetRedisindex, partition_key, range_key, value_size
RedisConnector.DeleteRedis
RedisConnector.ListRedisindex, limit
RedisConnector.LockRedislock_key, ttl_ms
RedisConnector.UnlockRedislock_key
RedisConnector.PublishCounterInt64Rediskey, value, updated_at, updated_by
DynamoDBConnector.SetDynamoDB
DynamoDBConnector.GetDynamoDB
DynamoDBConnector.DeleteDynamoDB
DynamoDBConnector.ListDynamoDB
DynamoDBConnector.LockDynamoDB
DynamoDBConnector.UnlockDynamoDB
DynamoDBConnector.getSimpleValueDynamoDBDetail span
PostgreSQLConnector.SetPostgreSQL
PostgreSQLConnector.GetPostgreSQL
PostgreSQLConnector.DeletePostgreSQL
PostgreSQLConnector.ListPostgreSQL
PostgreSQLConnector.LockPostgreSQL
PostgreSQLConnector.UnlockPostgreSQL
PostgreSQLConnector.PublishCounterInt64PostgreSQL
PostgreSQLConnector.getCurrentValuePostgreSQLDetail span
PostgreSQLConnector.getWithWildcardPostgreSQLDetail span
ConnectorFailsafe.GetFailsafeConnectorconnector.id, connector.partition_key, connector.range_key, failsafe.match_method, result.bytes
ConnectorFailsafe.SetFailsafeConnectorconnector.id, connector.partition_key, connector.range_key, failsafe.match_method, value.bytes, ttl.ms
ConnectorFailsafe.DeleteFailsafeConnector

Notable log messages

MessageLevelDriverMeaning
"redis is not connected (state: %s), errors: %v"WarnRedisEvery Get/Set/Lock attempt while connector is not ready.
"detected critical connection failure, marking for reconnection"WarnRedismarkConnectionAsLostIfNecessary fired; reconnect enqueued.
"successfully connected to Redis"InfoRedisPing succeeded after (re)connect.
"postgres connection lost; marking connector as failed for reinitialization"WarnPostgreSQLhandleConnectionFailure triggered reconnect.
"successfully connected to postgres"InfoPostgreSQLPool swap complete.
"migrating value column from TEXT to BYTEA" / "successfully migrated value column to BYTEA"InfoPostgreSQLOne-time schema migration.
"successfully configured pg_cron cleanup job"InfoPostgreSQLpg_cron extension detected and job installed.
"starting local expired items cleanup routine"DebugPostgreSQLpg_cron absent; local 5-minute ticker used instead.
"List operation on MemoryConnector is not efficiently supported"WarnMemoryList called on memory connector (unimplemented).
"gRPC client initialized for network"InfogRPCChain probe succeeded for a server.
"duplicate gRPC server for network detected; ignoring"ErrorgRPCTwo servers returned the same chainId.
"Starting Ristretto metrics collection loop"InfoMemoryemitMetrics=true, background goroutine started.

Source code entry points

Related pages

  • Cache policies — configure which methods get cached and with what TTL; connectors are selected here.
  • Real-time cache — the freshness gate that uses gRPC connector's CacheHeadReporter.
  • Retry — the same retry semantics available in failsafeForGets / failsafeForSets.
  • Circuit breaker — breaker semantics for connector-level failsafe; cache misses count as ignored, not failures.
  • Hedge — static-only hedge available at connector layer (quantile hedge is upstream-only).