/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: - id: my-redis # swap one line to change back-end: memory | redis | postgresql | dynamodb | grpc driver: redis redis: uri: redis://localhost:6379/0 connPoolSize: 8Agent 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 ConnectorConfig — common/config.go:L341-352
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
id | string | "<scope>-<driver>" e.g. "cache-memory" — common/defaults.go:L847-848 | Unique identifier; appears as the connector label on all metrics. |
driver | string | Inferred from whichever sub-config is present; explicit driver wins when both are set — common/defaults.go:L876-930 | One of: memory, redis, postgresql, dynamodb, grpc. Unknown value → ErrInvalidConnectorDriver at startup. |
failsafeForGets | []*FailsafeConfig | nil | Policies applied to Get. Evaluated by pickCacheExecutor in declaration order. |
failsafeForSets | []*FailsafeConfig | nil | Policies applied to Set and Delete. |
Each FailsafeConfig entry under failsafeForGets / failsafeForSets:
| Field | Type | Default | Notes |
|---|---|---|---|
matchMethod | string | "*" — common/defaults.go:L855-857 | Glob pattern matched against the request method. "*" = all methods. |
matchFinality | []DataFinalityState | nil (any) | Filter by finality; nil matches any finality. |
retry.maxAttempts | int | — | Retries only on isTransportError. |
retry.delay | Duration | — | Base delay between attempts. |
retry.backoffMaxDelay | Duration | — | Max delay with exponential backoff. |
retry.backoffFactor | float64 | — | Backoff multiplier per attempt. |
circuitBreaker.* | — | — | Standard breaker fields. ErrRecordNotFound and ErrRecordExpired count as OutcomeIgnore, not failures. |
timeout.duration | Duration | — | Per-operation timeout via context.WithTimeoutCause. |
hedge.delay | Duration | — | Static 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.maxCount | int | — | Max additional hedged copies. |
consensus | — | nil | Rejected 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
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
memory.maxItems | int | 100000 | Sets NumCounters = 3 × maxItems (ristretto frequency sketch). Must be > 0 or init fails. |
memory.maxTotalSize | string | "1GB" | Parsed by go-humanize; becomes ristretto MaxCost in bytes. Must be > 0. |
memory.emitMetrics | *bool | nil (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
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
redis.uri | string | — | Redis URI (redis:// or rediss://). Mutually exclusive with addr. |
redis.addr | string | — | host:port; converted to URI at defaults time then cleared. |
redis.username | string | — | Folded into URI; cleared afterward. |
redis.password | string | — | Redacted in logs/JSON. Folded into URI; cleared afterward. |
redis.db | int | 0 | Database index appended as /<db> in URI. |
redis.tls.enabled | bool | false | Uses rediss:// scheme; merges with URI-inferred TLS if both present. |
redis.tls.certFile | string | — | Client certificate for mTLS. |
redis.tls.keyFile | string | — | Client private key for mTLS. |
redis.tls.caFile | string | — | Custom CA bundle for server cert verification. |
redis.tls.insecureSkipVerify | bool | false | Skip server certificate verification. |
redis.connPoolSize | int | 8 | go-redis connection pool size. |
redis.initTimeout | Duration | 5s | Ping timeout per connection attempt. Falls back to DialTimeout from URI when zero. |
redis.getTimeout | Duration | 1s | Per-Get deadline. Falls back to ReadTimeout from URI when zero. |
redis.setTimeout | Duration | 3s | Per-Set/Delete deadline. Falls back to WriteTimeout from URI when zero. |
redis.lockRetryInterval | Duration | 500ms | Sleep between redsync mutex acquisition retries. |
redis.iamAuth.enabled | bool | false | Enables 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.cacheName | string | — | ElastiCache replication-group ID. Required when enabled. Lowercased automatically (AWS lowercases cache names at creation). |
redis.iamAuth.userID | string | — | ElastiCache user. Required when enabled. The user name and user ID must be identical — supply that single value. |
redis.iamAuth.region | string | derived | AWS region; derived from AWS_REGION / instance metadata when omitted. |
redis.iamAuth.auth.* | *AwsAuthConfig | nil (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
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
postgresql.connectionUri | string | — | pgx connection string (required). Redacted in logs. |
postgresql.table | string | "erpc_json_rpc_cache" (cache), "erpc_shared_state" (shared-state), "erpc_auth" (auth) | Table created on first connect. |
postgresql.minConns | int32 | 4 (non-auth), 1 (auth) | pgxpool MinConns. |
postgresql.maxConns | int32 | 32 (non-auth), 4 (auth) | pgxpool MaxConns. Pool lifecycle: MaxConnLifetime=5h, MaxConnIdleTime=30m. |
postgresql.initTimeout | Duration | 5s | Context deadline for pool creation. NOT read from URI — always explicit. |
postgresql.getTimeout | Duration | 1s | Per-Get/List query deadline. Never falls back to URI values. |
postgresql.setTimeout | Duration | 2s | Per-Set/Delete query deadline. Never falls back to URI values. |
postgresql.iamAuth.enabled | bool | false | Enables 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.region | string | derived | AWS region; derived from AWS_REGION / instance metadata when omitted. |
postgresql.iamAuth.endpoint | string | derived from connectionUri | host:port used to sign the token. Must include a port. — common/defaults.go:L1071-1090 |
postgresql.iamAuth.dbUser | string | derived from connectionUri | DB 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.* | *AwsAuthConfig | nil (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
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
dynamodb.table | string | "erpc_json_rpc_cache" / "erpc_shared_state" / "erpc_auth" | Created with PAY_PER_REQUEST if absent. |
dynamodb.region | string | — | AWS region (required; fatal if empty). |
dynamodb.endpoint | string | — | Custom endpoint (e.g., DynamoDB Local). |
dynamodb.auth.mode | string | — | "file" / "env" / "secret". Nil auth uses default credential chain. |
dynamodb.auth.credentialsFile | string | — | Path to AWS credentials file (mode=file). |
dynamodb.auth.profile | string | — | AWS profile name. |
dynamodb.auth.accessKeyID | string | — | Case-sensitive: struct tag is yaml:"accessKeyID" (capital I and D). Using accessKeyId (lowercase d) silently skips the field. — common/config.go:L483 |
dynamodb.auth.secretAccessKey | string | — | Static secret key (mode=secret). |
dynamodb.partitionKeyName | string | "groupKey" | DynamoDB partition key attribute name. |
dynamodb.rangeKeyName | string | "requestKey" | DynamoDB sort key attribute name. |
dynamodb.reverseIndexName | string | "idx_requestKey_groupKey" | GSI name for reverse lookups. Added if absent. |
dynamodb.ttlAttributeName | string | "ttl" | Numeric attribute holding unix epoch seconds for TTL. |
dynamodb.initTimeout | Duration | 5s | Session creation and table/index init timeout. |
dynamodb.getTimeout | Duration | 1s | Per-Get deadline. |
dynamodb.setTimeout | Duration | 2s | Per-Set/Delete/Lock deadline. |
dynamodb.maxRetries | int | 0 (SDK default) | AWS SDK retry count for transient errors. |
dynamodb.statePollInterval | Duration | 5s | WatchCounterInt64 polling interval. |
dynamodb.lockRetryInterval | Duration | no 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
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
grpc.bootstrap | string | — | HTTP URL returning JSON array or newline-separated gRPC server URLs. Fetched once at startup; failure → connector init error. |
grpc.servers | []string | — | Explicit server list. Combined with bootstrap results when both present. |
grpc.headers | map[string]string | — | Headers sent on every gRPC call. |
grpc.getTimeout | Duration | 100ms | Per-Get call timeout. Applied only when shorter than existing context deadline. |
Timeout buffer constants — data/timeout_constants.go:L7-15
| Constant | Value | Usage |
|---|---|---|
DefaultOperationBuffer | 10s | Time reserved after lock acquisition for in-lock operations. Used by Redis lock retry budget calculation. |
PollOperationBuffer | 15s | Reserved in the state poller after lock acquisition. |
MinPollTimeout | 30s | Minimum 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: - 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: - 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: 13. 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: - 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: 164. 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: # 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: - 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: - 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 / metadataThe 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
NewConnectordispatches oncfg.Driver. Unknown driver →ErrInvalidConnectorDriverat startup; never returned during live request processing. [data/connector.go:L63-103]Setalways writesConnectorMainIndexand, forevm:-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
Getand returnsErrRecordExpiredfor items past TTL regardless of AWS purge lag. Callers must distinguishErrRecordExpiredfromErrRecordNotFound. [data/dynamodb.go:L547-556] - gRPC intercepts
eth_blockNumberand translates it toeth_getBlockByNumber("latest", false), returning only thenumberfield as a canonical hex string. On failure returnsErrRecordNotFound(falls through to real upstreams). [data/grpc.go:L265-278] FailsafeConnectorbypasses failsafe policies forList,Lock,WatchCounterInt64, andPublishCounterInt64. OnlyGet,Set, andDeleteget failsafe treatment. [data/failsafe.go:L308-322]FailsafeConnectorforwardsCacheLatestBlockTimestampto the wrapped connector so the freshness gate works even when the gRPC connector is wrapped. [data/failsafe.go:L211-216]isTransportErroris the sole retryability gate:ErrRecordNotFound,ErrRecordExpired,context.Canceled, andcontext.DeadlineExceededare not transport errors and are never retried. [data/failsafe.go:L25-92]pickCacheExecutorfour-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
Listis unimplemented; any shared-state or admin endpoint that needsListsilently breaks with memory. - Set
dynamodb.lockRetryInterval: 100msexplicitly in production. DynamoDB has no default; zero means the retry loop spins as fast as the HTTP API responds under contention. Redis defaults to500ms. - Set PostgreSQL timeouts explicitly; do not rely on the connection URI. Unlike Redis,
initTimeout,getTimeout, andsetTimeoutare always Go context deadlines and are never read fromconnectionUri. Settingconnect_timeout=5in the DSN has no effect on these fields. - Always supply a
caFileor client cert when usingrediss://in production. Arediss://-only URI without a YAMLtls:block producesInsecureSkipVerify=true. - Never use
dynamodb.auth.accessKeyId(lowercased). The struct tag isyaml:"accessKeyID"— a lowercasedsilently 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
CacheLatestBlockTimestampreturnsok=falseand the freshness gate fails open — stale realtime responses may be served. - Wrap the gRPC connector with a circuit-breaker in
failsafeForGetsto 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.iamAuthenabled andiamAuth.authomitted, 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
-
Memory
Listis not implemented. Returns an error with guidance to use Redis/PostgreSQL/DynamoDB. Any subsystem needingList(admin endpoints, shared-state enumeration) must not use the memory driver.data/memory.go:L316-L321(opens in a new tab) -
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) -
Memory cost overhead is 256 bytes per entry. Even a 1-byte value costs 257 bytes against
maxTotalSize. Small-value workloads require a proportionally largermaxTotalSize.data/memory.go:L78-L80(opens in a new tab) -
DynamoDB TTL lag (~48h). AWS native TTL expiry is eventually consistent. The connector applies a client-side epoch check returning
ErrRecordExpired, but callers must distinguishErrRecordExpiredfromErrRecordNotFoundsince the expired item still exists in DynamoDB.data/dynamodb.go:L547-L556(opens in a new tab) -
DynamoDB
lockRetryIntervalhas no default. Zero duration → retry loop spins as fast as the DynamoDB API under contention. Setdynamodb.lockRetryInterval: 100msin production. Contrast: Redis defaults to500ms.data/dynamodb.go:L660-L688(opens in a new tab) -
PostgreSQL advisory lock is non-blocking.
pg_try_advisory_xact_lockreturns immediately withfalseif locked. No built-in retry loop — callers must implement their own.data/postgresql.go:L558-L573(opens in a new tab) -
PostgreSQL timeouts cannot be set via connection URI. Unlike Redis,
initTimeout,getTimeout, andsetTimeoutare always Go context deadlines. Settingconnect_timeout=5in the DSN does NOT populateinitTimeout. Configure all three explicitly in YAML.data/postgresql.go:L130-L134(opens in a new tab) -
PostgreSQL
cron.scheduleis not idempotent.applySchemais gated by aschemaAppliedflag (set once per process lifetime) to prevent duplicate pg_cron jobs on reconnect.data/postgresql.go:L257-L268(opens in a new tab) -
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) -
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) -
gRPC bootstrap fetch is not retried. If
fetchGrpcServersfails at startup,NewGrpcConnectorreturns an error. Individual per-server connection tasks ARE retried by the initializer.data/grpc.go:L88-L95(opens in a new tab) -
gRPC freshness gate fails open for non-gRPC connectors. Memory, Redis, PostgreSQL, and DynamoDB do not implement
CacheHeadReporter. Wheneth_blockNumberoreth_gasPriceis fetched from one of these connectors,shouldAcceptCachedResultcannot determine response age and unconditionally accepts the cached result.architecture/evm/json_rpc_cache.go:L862-L920(opens in a new tab) -
gRPC block-head poller: zero
latestTsmeans "unknown", not genesis block. Wheneth_getBlockByNumber("latest")returns a block with a zero or absenttimestampfield,CacheLatestBlockTimestampreturnsok=falseand the freshness gate fails open.data/grpc.go:L349-L357(opens in a new tab) -
Hedge quantile rejected for connector-level failsafe. Only static
delay.base/delay.minwork.hedge.delay.quantile > 0fails validation with"hedge quantile is not supported for connector-level failsafe".data/cache_executor.go:L37-L41(opens in a new tab) -
Consensus rejected for connector-level failsafe.
consensusnon-nil at construction →ErrFailsafeConfigurationat startup.data/cache_executor.go:L31-L35(opens in a new tab) -
dynamodb.auth.accessKeyIDis case-sensitive. The struct tag isyaml:"accessKeyID"(capitalIandD). UsingaccessKeyId(lowercased) silently skips the field; AWS falls through to the default credential chain.common/config.go:L483(opens in a new tab) -
Redis TLS merging with
rediss://URIs. When both arediss://URI andtls.enabled: trueare configured, YAML cert/CA overrides merge onto the URI-derived TLS baseline and disableInsecureSkipVerify. Arediss://-only URI without a YAMLtls:block producesInsecureSkipVerify=true. Always supply acaFileor client cert when usingrediss://in production.data/redis.go:L156-L180(opens in a new tab) -
FailsafeConnectorwrapping does not breakCacheHeadReporter.FailsafeConnectorforwardsCacheLatestBlockTimestampto 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) -
gRPC freshness gate: response timestamp takes precedence over connector head.
shouldAcceptCachedResultfirst extracts a block timestamp from the cached response itself. Only if the response carries no timestamp does it fall back toCacheLatestBlockTimestamp. Foreth_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) -
PostgreSQL pub/sub falls back to 30-second polling. If
LISTEN/NOTIFYis unavailable (e.g., connection pooler that strips it),WatchCounterInt64falls 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) -
DynamoDB backward compatibility:
SandBattribute 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) -
PostgreSQL pool lock timing. During reconnect,
connectTaskholds 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) -
IAM auth + static password is rejected at startup. Redis rejects a password embedded in the URI when
iamAuth.enabled: true; PostgreSQL rejects a password inconnectionUri. Remove the password — the IAM token is injected at connect time.common/validation.go:L556-L630(opens in a new tab) -
ElastiCache
userIDmust equal the user name. For IAM-enabled ElastiCache users the user name and user ID are identical by AWS requirement; supply that single value asuserID. A mismatch authenticates as the wrong user or fails.cacheNameis lowercased automatically (AWS lowercases at creation), but the ElastiCache user must still matchuserIDexactly.common/defaults.go:L971-L982(opens in a new tab) -
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-invokingCredentialsProviderContextfor a fresh token — before the cliff. Do not override the pool lifetime when IAM auth is on. ElastiCache Serverless is unsupported (noPSUBSCRIBE).data/redis_iam_auth.go:L22-L24(opens in a new tab) -
RDS
iamAuth.dbUsermust match theconnectionUriuser. When both are set and differ, startup fails — pgx would otherwise sign the token fordbUserwhile connecting as the URI user, a silent auth mismatch. The user must holdrds_iamand SSL is mandatory (sslmode=requireis appended automatically; an explicit weakersslmodeis rejected).common/validation.go:L556-L590(opens in a new tab) -
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
| Metric | Type | Labels | When it fires |
|---|---|---|---|
erpc_ristretto_cache_current_cost | gauge | connector | Every 30s by memory connector when emitMetrics=true. Bytes currently used by ristretto. |
erpc_ristretto_cache_sets_failed_total | counter | connector | Every 30s; delta of SetsDropped + SetsRejected from ristretto stats. |
erpc_cache_connector_earliest_block_number | gauge | connector, network | Set every 60s when gRPC fetchTaggedBlock("earliest") succeeds. Drives fast-miss rejection. Not reset on transient poll failure. |
erpc_cache_connector_latest_block_number | gauge | connector, network | Set every 60s when gRPC fetchTaggedBlock("latest") succeeds. |
erpc_cache_connector_finalized_block_number | gauge | connector, network | Set every 60s when finalized block is fetchable. |
erpc_cache_connector_earliest_block_timestamp_seconds | gauge | connector, network | Unix timestamp (seconds) of the earliest known block. Set only when block timestamp > 0. |
erpc_cache_connector_latest_block_timestamp_seconds | gauge | connector, network | Unix 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_seconds | gauge | connector, network | Unix timestamp of the finalized block. Set only when block timestamp > 0. |
Trace spans
| Span name | Connector | Notable attributes |
|---|---|---|
RedisConnector.Set | Redis | partition_key, range_key, value_size |
RedisConnector.Get | Redis | index, partition_key, range_key, value_size |
RedisConnector.Delete | Redis | |
RedisConnector.List | Redis | index, limit |
RedisConnector.Lock | Redis | lock_key, ttl_ms |
RedisConnector.Unlock | Redis | lock_key |
RedisConnector.PublishCounterInt64 | Redis | key, value, updated_at, updated_by |
DynamoDBConnector.Set | DynamoDB | |
DynamoDBConnector.Get | DynamoDB | |
DynamoDBConnector.Delete | DynamoDB | |
DynamoDBConnector.List | DynamoDB | |
DynamoDBConnector.Lock | DynamoDB | |
DynamoDBConnector.Unlock | DynamoDB | |
DynamoDBConnector.getSimpleValue | DynamoDB | Detail span |
PostgreSQLConnector.Set | PostgreSQL | |
PostgreSQLConnector.Get | PostgreSQL | |
PostgreSQLConnector.Delete | PostgreSQL | |
PostgreSQLConnector.List | PostgreSQL | |
PostgreSQLConnector.Lock | PostgreSQL | |
PostgreSQLConnector.Unlock | PostgreSQL | |
PostgreSQLConnector.PublishCounterInt64 | PostgreSQL | |
PostgreSQLConnector.getCurrentValue | PostgreSQL | Detail span |
PostgreSQLConnector.getWithWildcard | PostgreSQL | Detail span |
ConnectorFailsafe.Get | FailsafeConnector | connector.id, connector.partition_key, connector.range_key, failsafe.match_method, result.bytes |
ConnectorFailsafe.Set | FailsafeConnector | connector.id, connector.partition_key, connector.range_key, failsafe.match_method, value.bytes, ttl.ms |
ConnectorFailsafe.Delete | FailsafeConnector |
Notable log messages
| Message | Level | Driver | Meaning |
|---|---|---|---|
"redis is not connected (state: %s), errors: %v" | Warn | Redis | Every Get/Set/Lock attempt while connector is not ready. |
"detected critical connection failure, marking for reconnection" | Warn | Redis | markConnectionAsLostIfNecessary fired; reconnect enqueued. |
"successfully connected to Redis" | Info | Redis | Ping succeeded after (re)connect. |
"postgres connection lost; marking connector as failed for reinitialization" | Warn | PostgreSQL | handleConnectionFailure triggered reconnect. |
"successfully connected to postgres" | Info | PostgreSQL | Pool swap complete. |
"migrating value column from TEXT to BYTEA" / "successfully migrated value column to BYTEA" | Info | PostgreSQL | One-time schema migration. |
"successfully configured pg_cron cleanup job" | Info | PostgreSQL | pg_cron extension detected and job installed. |
"starting local expired items cleanup routine" | Debug | PostgreSQL | pg_cron absent; local 5-minute ticker used instead. |
"List operation on MemoryConnector is not efficiently supported" | Warn | Memory | List called on memory connector (unimplemented). |
"gRPC client initialized for network" | Info | gRPC | Chain probe succeeded for a server. |
"duplicate gRPC server for network detected; ignoring" | Error | gRPC | Two servers returned the same chainId. |
"Starting Ristretto metrics collection loop" | Info | Memory | emitMetrics=true, background goroutine started. |
Source code entry points
data/connector.go:L40-L103(opens in a new tab) —Connectorinterface;CacheHeadReporter;NewConnectorfactory;ConnectorMainIndex/ConnectorReverseIndexconstantsdata/memory.go:L1-L321(opens in a new tab) — ristretto-backed connector; TTL; reverse index; no-op pub/sub; TryLock; background metrics; unimplementedListdata/redis.go:L1-L784(opens in a new tab) — Redis connector; URI construction; TLS merging; narrow reconnect detection; redsync locking; SCAN-basedListdata/redis_pubsub_manager.go(opens in a new tab) — self-healing pub/sub manager; transparent reconnection; copy-on-write subscriber managementdata/timeout_constants.go(opens in a new tab) —DefaultOperationBuffer(10s),PollOperationBuffer(15s),MinPollTimeout(30s)data/postgresql.go:L1-L1270(opens in a new tab) — pgxpool connector; schema migration; pg_cron or local cleanup ticker; advisory locks; LISTEN/NOTIFY pub/sub; connection failure coalescingdata/dynamodb.go:L1-L983(opens in a new tab) — DynamoDB connector; split read/write clients; GSI; native TTL + client-side expiry check; conditional-PutItem locking; pollingWatchCounterInt64data/grpc.go:L1-L478(opens in a new tab) — Read-only gRPC BDS connector; bootstrap; chain probing;eth_blockNumbertranslation; fast-miss rejection; 60s head poller;CacheLatestBlockTimestampdata/failsafe.go:L25-L216(opens in a new tab) —FailsafeConnector;pickCacheExecutorfour-tier selection;isTransportError;CacheHeadReporterforwardingdata/cache_executor.go:L1-L160(opens in a new tab) —cacheExecutorretry/hedge/breaker/timeout pipeline; transport-error-only retry; consensus/hedge-quantile rejectionarchitecture/evm/json_rpc_cache.go:L834-L924(opens in a new tab) —shouldAcceptCachedResult: freshness gate; response-timestamp path;CacheHeadReporterfallback; fail-open logiccommon/defaults.go:L846-L1099(opens in a new tab) —SetDefaultsfor all connector configs; driver inference; default table names by scope; IAM auto-TLS (Redis) andsslmode=require+ endpoint/dbUser derivation (PostgreSQL)data/aws_auth.go:L1-L42(opens in a new tab) — sharedcreateAWSSessionhelper; resolvesiamAuth.authmodes or the AWS SDK default credential chaindata/redis_iam_auth.go:L1-L86(opens in a new tab) — ElastiCache SigV4 presign; scheme stripping;CredentialsProviderContext; 11h connection-lifetime constantsdata/postgresql_iam_auth.go:L1-L39(opens in a new tab) — RDSrdsutils.BuildAuthTokenminted inside the pgxpoolBeforeConnecthook
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).