Config
Database
EVM Cache

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.

erpc.yaml
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:

erpc.yaml
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:

erpc.yaml
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 empty
  • 0x (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:

  1. 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
  2. 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
  3. 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:

erpc.yaml
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.