evmJsonRpcCache
This feature defines the destination for caching JSON-RPC calls towards any EVM architecture upstream. Caching mechanism is non-blocking on critical path, and is used as best-effort. If the database is not available, the cache set/get will be skipped.
database:
evmJsonRpcCache:
# Backend storage connectors where to store the cache
connectors:
- id: string
driver: memory | redis | postgresql | dynamodb
# ... (driver specific config, see below)
# Cache policies for different network/method/finality states
policies:
- network: string # Optional (default: "*")
method: string # Optional (default: "*")
params: []any # Optional
finality: finalized | unfinalized | realtime | unknown # Optional (default: finalized)
empty: ignore | allow | only # Optional (default: ignore)
minItemSize: string # Optional - xB | xKB | xMB
maxItemSize: string # Optional - xB | xKB | xMB
connector: string # Required
ttl: duration # Optional (default: "0" means forever) - 100ms, 5s, 1m, ...
# Optional cache methods configuration to override default supported methods
# These are used to understand nature of each method and where to find the block reference.
# Scroll to bottom of this page for default supported methods and examples.
methods:
<method_name>:
reqRefs: [][]any # Optional - array of path to potential palce of block number/hash in request.params
respRefs: [][]any # Optional - array of path to potential palce of block number/hash in response.result
finalized: bool # Optional - this method always returns finalized data (e.g. eth_chainId)
realtime: bool # Optional - this method always returns realtime data (e.g. eth_gasPrice)
Make sure the storage requirements meet your usage, for example caching 70m blocks + 10m txs + 10m traces on Arbitrum needs 200GB of storage.
The cache config allows you to define multiple connectors (storage backends) and policies for different finality states. Here's the basic structure:
database:
evmJsonRpcCache:
# Define one or more storage connectors with unique IDs useful in policies
connectors:
- id: memory-cache
driver: memory # Refer to "memory" driver docs below
memory:
maxItems: 100000
- id: redis-cache-local
driver: redis # Refer to "redis" driver docs below
redis:
addr: localhost:6379
password: "xxxxxxxxx"
db: 0
connPoolSize: 128
- id: redis-cache-momento
driver: redis # Refer to "redis" driver docs below
redis:
addr: momento.aws.momentohq.com:6379
password: "xxxxxxxxx"
db: 0
connPoolSize: 128
- id: postgres-cache
driver: postgresql # Refer to "postgresql" driver docs below
postgresql:
connectionUri: >-
postgres://YOUR_USERNAME_HERE:YOUR_PASSWORD_HERE@your.postgres.hostname.here.com:5432/your_database_name
table: rpc_cache
# ... any driver can be used multiple times
# Define caching policies for different network/method/finality states
policies:
# Example: Cache all methods with finalized data including empty responses
- network: "*"
method: "*"
finality: finalized
empty: allow
connector: memory-cache
ttl: 0
# Example: Cache unfinalized data only for 5 seconds (getLogs of a recent block) except empty responses
- network: "*"
method: "*"
finality: unfinalized
empty: ignore
connector: memory-cache
ttl: 5s
# Example: Cache unknown finalization data (eth_trace*) only for 10 seconds
- network: "*"
method: "*"
finality: unknown
empty: ignore
connector: memory-cache
ttl: 10s
# Example: Cache realtime data only for 2 seconds (eth_blockNumber, eth_gasPrice, etc) to reduce costs yet fresh enough data
- network: "*"
method: "*"
finality: realtime
empty: ignore
connector: memory-cache
ttl: 2s
- network: "*" # "network" supports * as wildcard and | as OR operator
method: "eth_getLogs | trace_*" # "method" supports * as wildcard and | as OR operator
finality: finalized
empty: allow
connector: postgres-cache
ttl: 0
- network: "evm:42161 | evm:10"
method: "arbtrace_*"
finality: finalized
empty: ignore
connector: postgres-cache
ttl: 1d
Cache policies
You can create multiple policies to define different caching behavior for different networks, methods, finality state, emptyish checks, and item size limits.
- On each cache "set" operation all policies that match the network/method/finality state will be used to store the data.
- On each cache "get" operation all policies that match the network/method will be used to retrieve the data, from top to bottom as defined in the config, the first policy that returns a cache hit will be used.
Policy matching
Each policy can define matching rules for:
policies:
- network: "evm:42161 | evm:10" # (OPTIONAL) Network ID matching
method: "eth_getLogs | trace_*" # (OPTIONAL) Method name matching
params: # (OPTIONAL) parameter matching
- ">=0x100 | <=0x200" # First parameter
- "*" # Second parameter
# ... additional param matchers
finality: finalized
empty: ignore
minItemSize: 10b
maxItemSize: 100mb
connector: postgres-cache
ttl: 1d
#
# The `params` field allows you to define matching rules for RPC method parameters. This is useful for creating granular caching policies based on specific parameter values:
#
# Cache eth_getLogs requests for specific block ranges
- network: "*"
method: "eth_getLogs"
params:
- fromBlock: ">=0x100"
toBlock: "<=0x200"
finality: finalized
connector: postgres-cache
ttl: 1d
# Cache eth_getBlockByNumber for specific blocks
- network: "*"
method: "eth_getBlockByNumber"
params:
- ">=0x100 | <=0x200" # Block number range
- "*" # Include details flag
finality: finalized
connector: redis-cache
ttl: 1h
#
# More examples for params matching:
#
# Match specific block numbers
params: ["0x1 | 0x2 | 0x3", "*"]
# Match block number ranges
params: [">=0x100 | <=0x200", "*"]
# Match eth_getLogs with specific criteria
params:
- fromBlock: ">=0x100"
toBlock: "<=0x200"
address: "*"
topics: ["*"]
# Match array parameters:
params: [[">0x123", ">=0x456"], "*"]
# Match empty parameters:
params: ["*", "<empty>"]
The parameter matcher supports:
- Wildcards: Use
*
to match any value - OR operator: Use
|
to specify multiple valid values - Numeric comparisons: For hex/decimal numbers:
>value
- Greater than>=value
- Greater than or equal<value
- Less than<=value
- Less than or equal
- Object matching: For nested parameter objects (like in eth_getLogs), you can specify matchers for individual fields
- Array matching: For array parameters, each element can have its own matcher
- Empty values: Use
<empty>
to match null/undefined values
finality
states
The cache system recognizes three finality states:
finalized
: (default) Data from blocks that are confirmed as finalized (safe to cache long-term). This is based on 'finalized' block fetched via eth_getBlockByNumber of the upstream corresponding to the received response (not other upstreams).unfinalized
: Data from recent blocks that could still be reorged. Also any data/transaction from pending blocks is considered unfinalized.realtime
: Data that is expected to be updated on every new block (e.g. eth_blockNumber, eth_gasPrice, eth_maxPriorityFeePerGas, etc). You must use a short TTL (i.e. 2 * block time) to ensure it's fresh enough.unknown
: When block number cannot be determined from request/response (e.g.,eth_traceTransaction
). Most often it is safe to cache this data without reorg safety because they are not referenced by final actual blocks (e.g. eth_getTransactionByHash).
empty
states
The cache can match three empty states:
ignore
: (default) Ignore empty responses and do not cache them.allow
: Allow caching empty responses as well.only
: Only cache empty responses, e.g. if you want to give different TTL.
These values are considered empty:
null
for example for a non-existent block[]
(empty array) for example for an empty array from eth_getLogs{}
(empty object) for example when trace results is empty0x
(empty hex) for example for an empty string from eth_getCode or eth_call
Re-org mechanism
The cache system provides mechanisms to handle blockchain reorganizations (re-orgs) through the finality state matchers and TTL settings. Here are the key strategies:
-
Finalized data caching
- Use the
finality: finalized
matcher for data that is confirmed and safe from re-orgs - This data can be cached with long or infinite TTL (
ttl: 0
) - Example: Historical block data, old transaction receipts
- Use the
-
Unfinalized data caching
- Use
finality: unfinalized
for recent blocks that could be re-orged - Set short TTL values (10-30 seconds recommended)
- Example: Recent blocks, pending transactions
- network: "*" method: "eth_getBlockByNumber" finality: unfinalized connector: memory-cache ttl: 5s
- Use
-
Mixed strategy example You can combine multiple policies for the same method:
policies: # Cache finalized blocks forever - network: "*" method: "eth_getBlockByNumber" finality: finalized connector: postgres-cache ttl: 0 # Cache unfinalized blocks briefly - network: "*" method: "eth_getBlockByNumber" finality: unfinalized connector: memory-cache ttl: 5s
This approach is useful for various scenarios:
- Caching gas estimates briefly to reduce RPC calls
- Temporarily storing
eth_blockNumber
results - Balancing between performance and data consistency
Make sure to properly test your dApps/indexer full flow to ensure unfinalized data caching works as expected.
For chains which do not support "finalized" block method, eRPC will consider last 1024 blocks unfinalized. This number can be configured via network.evm.fallbackFinalityDepth
.
Cacheable methods
Methods are cached if they include a blockNumber
or blockHash
in the request or response, allowing cache invalidation during blockchain reorgs.
If no blockNumber is present, caching is still viable if the method returns data unaffected by reorgs, like eth_chainId
, or if the data won't change after a reorg, such as eth_getTransactionReceipt
.
By default, eRPC comes with pre-configured method caching rules. Here's the default configuration:
database:
evmJsonRpcCache:
# Here are default supported methods and their configuration:
methods:
# Static methods that return fixed values:
eth_chainId:
finalized: true
net_version:
finalized: true
# Realtime methods that change frequently (i.e. on every block):
eth_hashrate:
realtime: true
eth_mining:
realtime: true
eth_syncing:
realtime: true
net_peerCount:
realtime: true
eth_gasPrice:
realtime: true
eth_maxPriorityFeePerGas:
realtime: true
eth_blobBaseFee:
realtime: true
eth_blockNumber:
realtime: true
erigon_blockNumber:
realtime: true
# Methods with block references in request/response:
# Make sure number is first in the array if hash is also present
eth_getLogs:
reqRefs:
- [0, fromBlock]
- [0, toBlock]
- [0, blockHash]
eth_getBlockByHash:
reqRefs: [[0]]
respRefs: [[number], [hash]]
eth_getBlockByNumber:
reqRefs: [[0]]
respRefs: [[number], [hash]]
eth_getTransactionByBlockHashAndIndex:
reqRefs: [[0]]
respRefs: [[blockNumber], [blockHash]]
eth_getTransactionByBlockNumberAndIndex:
reqRefs: [[0]]
respRefs: [[blockNumber], [blockHash]]
eth_getUncleByBlockHashAndIndex:
reqRefs: [[0]]
respRefs: [[number], [hash]]
eth_getUncleByBlockNumberAndIndex:
reqRefs: [[0]]
respRefs: [[number], [hash]]
eth_getBlockTransactionCountByHash:
reqRefs: [[0]]
eth_getBlockTransactionCountByNumber:
reqRefs: [[0]]
eth_getUncleCountByBlockHash:
reqRefs: [[0]]
eth_getUncleCountByBlockNumber:
reqRefs: [[0]]
eth_getStorageAt:
reqRefs: [[2]]
eth_getBalance:
reqRefs: [[1]]
eth_getTransactionCount:
reqRefs: [[1]]
eth_getCode:
reqRefs: [[1]]
eth_call:
reqRefs: [[1]]
eth_getProof:
reqRefs: [[2]]
arbtrace_call:
reqRefs: [[2]]
eth_feeHistory:
reqRefs: [[1]]
eth_getAccount:
reqRefs: [[1]]
eth_estimateGas:
reqRefs: [[1]]
debug_traceCall:
reqRefs: [[1]]
eth_simulateV1:
reqRefs: [[1]]
erigon_getBlockByTimestamp:
reqRefs: [[1]]
arbtrace_callMany:
reqRefs: [[1]]
eth_getBlockReceipts:
reqRefs: [[0]]
trace_block:
reqRefs: [[0]]
debug_traceBlockByNumber:
reqRefs: [[0]]
trace_replayBlockTransactions:
reqRefs: [[0]]
debug_storageRangeAt:
reqRefs: [[0]]
debug_traceBlockByHash:
reqRefs: [[0]]
debug_getRawBlock:
reqRefs: [[0]]
debug_getRawHeader:
reqRefs: [[0]]
debug_getRawReceipts:
reqRefs: [[0]]
erigon_getHeaderByNumber:
reqRefs: [[0]]
arbtrace_block:
reqRefs: [[0]]
arbtrace_replayBlockTransactions:
reqRefs: [[0]]
# Special methods that can be cached regardless of block:
# Most often finality of these responses is 'unknown'.
# For these data it is safe to keep the data in cache even after reorg,
# because if client explcitly querying such data (e.g. a specific tx hash receipt)
# they know it might be reorged from a separate process.
# For example this is not safe to do for eth_getBlockByNumber because users
# require the method to always give them current accurate data (even if it's reorged).
# Using "*" as request blockRef means that these data are safe be cached irrevelant of their block.
eth_getTransactionReceipt:
reqRefs: [["*"]]
respRefs: [[blockNumber], [blockHash]]
eth_getTransactionByHash:
reqRefs: [["*"]]
respRefs: [[blockNumber], [blockHash]]
arbtrace_replayTransaction:
reqRefs: [["*"]]
trace_replayTransaction:
reqRefs: [["*"]]
debug_traceTransaction:
reqRefs: [["*"]]
trace_rawTransaction:
reqRefs: [["*"]]
trace_transaction:
reqRefs: [["*"]]
debug_traceBlock:
reqRefs: [["*"]]
To customize the cacheable methods, you can override the default configuration. Note that if you customize the methods, you must include ALL methods you want to cache - the defaults will not be merged.
When customizing methods, make sure to include all methods you want to cache. The default configuration will be completely replaced by your custom configuration.
Here's how method configuration works:
finalized: true
- Method returns static data that never changes.realtime: true
- Method returns data that changes frequently (e.g. on every block).reqRefs
- Array of paths to find block numbers/hashes in the request.respRefs
- Array of paths to find block numbers/hashes in the response.- Special value
[["*"]]
means the method can be cached regardless of block reorgs.