Anvil state forking: why RPC caching speeds up local tests
Anvil fork tests become slow when the same remote storage slots are fetched repeatedly. The failure is not in EVM execution. It is in the boundary between the local test process and the upstream JSON-RPC provider.

A forked Anvil node does not begin with a complete copy of a chain. It reconstructs state on demand. When execution touches an account balance, contract bytecode, or storage slot that is not available locally, Anvil requests that value from the upstream RPC endpoint. Without effective caching, the same test suite can generate the same remote reads on every run.
That creates redundant network round-trips. It consumes provider compute units. It introduces latency that has nothing to do with the contract under test.
Anvil state forking with RPC caching changes the execution profile. Remote state is fetched once, persisted locally, and reused by later reads. The fork still behaves as a local chain. The source of untouched state remains the upstream chain. The repeated transport cost is removed.
Forking is local execution over lazily fetched remote state. RPC caching removes repeated reads. It does not create a second mainnet.
The mechanics of Anvil state forking and remote state fetching
A normal Anvil instance starts from a local genesis state. The accounts, balances, bytecode, and storage are created inside the local node.
Fork mode is different. Anvil starts from a selected upstream chain state, usually through a command such as:
anvil --fork-url <RPC_URL> --fork-block-number <BLOCK>
The --fork-url parameter identifies the upstream JSON-RPC endpoint. The optional --fork-block-number pins the fork to a specific block. That pin matters. A test that runs against a moving head is not deterministic at the state boundary. Balances, contract storage, token ownership, and protocol configuration can change between runs.
The forked node does not download every account and every storage slot at startup. That would be expensive and often impossible for a large chain. Instead, the backend resolves state lazily.
A contract call can touch several distinct state categories:
- The account balance used by an opcode such as
BALANCE. - The contract bytecode required for execution.
- A storage slot read through
SLOAD. - Account metadata required to determine whether an address exists or contains code.
- State belonging to another contract reached through a call.
If the value is not already present in the local fork database, Anvil requests it from the upstream provider. The fetched value becomes available to the local execution environment. Later reads can use the local copy.
The relevant implementation model relies on Foundry’s foundry_fork_db backend. Its role is not limited to storing arbitrary test output. It deduplicates provider requests and caches remote state associated with the fork. The distinction matters because an RPC cache is a state-resolution layer. It is not the same object as a persisted Anvil node snapshot.
The local chain applies state mutations on top of the forked base. A test can deploy a contract, approve a token, alter a balance through a mock, or execute a protocol transaction. Those writes affect the local fork only. They do not modify the remote mainnet or testnet chain.
The resulting state model has two layers:
1. Remote base state. This is read from the upstream RPC endpoint when required.
2. Local mutation layer. This contains transactions and state changes produced by the test process.
The local mutation layer has precedence during execution. If a test writes to a storage slot and reads it again, the read returns the locally mutated value. The upstream provider is not contacted to replace that local state.
This is a copy-on-write model. Anvil does not clone the full remote chain. It reads the required portions and applies local changes over them.
Why the fork block is part of the test input
A block-pinned fork defines a state boundary. The same URL at a different block can represent a different protocol configuration.
Consider a test that reads:
- A Uniswap pool’s reserves.
- An ERC-20 token’s total supply.
- A lending market’s interest-rate configuration.
- A governance contract’s proposal state.
- A proxy’s implementation address.
Those values are block-dependent. If the fork starts at the latest block, the test input changes as the chain advances. A local cache cannot correct that. It can only make repeated reads of the selected state cheaper.
The --fork-block-number option therefore has two functions:
- It reduces test drift caused by upstream chain progress.
- It gives the RPC cache a stable chain-and-block identity.
The cache is useful only when the state being requested corresponds to the fork that the test expects. A cache entry from one block is not a valid substitute for another block unless the system explicitly treats it as such. State identity is part of correctness.
How local RPC caching removes redundant round-trips
Without a local cache, the execution path for a storage read is simple:
1. The EVM reaches an SLOAD.
2. The fork database checks whether the slot is known locally.
3. The value is missing.
4. Anvil sends a request to the upstream JSON-RPC provider.
5. The provider returns the slot value.
6. Execution continues.
The network request is the expensive part. The local SLOAD is not.
The same pattern appears when a test suite creates several fresh Anvil processes against the same fork. Each process may need to resolve the same remote state. A protocol integration test can repeatedly access the same token balances, pool slots, proxy implementation slots, and oracle data. These values are stable for a block-pinned fork, but the provider still has to serve them unless they have been cached locally.
With RPC caching enabled, the second execution path changes:
1. The EVM reaches the storage read.
2. The fork database checks the local cache.
3. The slot is found.
4. Anvil uses the cached value.
5. No upstream request is required.
This reduces three types of cost.
Network latency
Every remote request introduces transport overhead. The test process must communicate with the provider, wait for the provider to process the request, receive the response, and decode it. The delay depends on the endpoint, geographic distance, connection reuse, provider load, and request volume.
The cache removes that path after the value has been stored locally. The remaining operation is local file or memory access followed by EVM execution.
Provider limits
RPC providers usually meter access through request quotas or compute-unit accounting. Anvil’s documented default assumed compute rate is 330 compute units per second. The exact accounting behavior depends on the provider and request type, but the operational issue is consistent: repeated state reads consume external capacity.
Cached reads do not require another upstream JSON-RPC request. That reduces pressure on the provider and lowers the risk of rate-limit failures during repeated test runs.
Test variance
A test that depends on an external RPC endpoint inherits external failures. The endpoint can throttle the connection, return an error, become slow, or temporarily fail. A local cache does not eliminate the initial dependency. It limits how often the dependency is exercised.
The first run still has to fetch uncached state. Later runs can reuse it. The performance improvement is therefore workload-dependent. A test suite that touches a large number of unique storage slots still performs many initial remote reads. A suite that repeatedly touches the same state benefits more.
There is no universal percentage reduction in test duration. The result depends on provider latency, the number of distinct reads, the number of test processes, and how much state is reused between runs. The deterministic point is narrower: cached state eliminates redundant provider requests for values already present in the local cache.
Managing the storage cache
By default, Anvil stores the RPC fork cache on disk under:
~/.foundry/cache/rpc/<chain>/<block>/
The directory contains storage.json. This path represents the cached remote state associated with the chain and fork block.
The layout exposes an important boundary. The cache is not simply a global bag of values keyed by contract address. It is associated with the fork identity. Chain and block selection determine which remote state is valid for the test.
A typical debugging sequence starts with the fork definition, not the cache file:
- Confirm the upstream RPC endpoint.
- Confirm the chain selected by the fork.
- Confirm the
--fork-block-numbervalue. - Confirm that the expected cache directory exists.
- Inspect whether
storage.jsonis being populated after a test touches remote state. - Re-run the same test and compare provider activity.
The cache can become large when the suite touches many contracts and slots. That is expected. Storage access is granular. A single contract may occupy many relevant slots, especially when it contains mappings, packed variables, dynamic arrays, or proxy-related state.
Disabling storage caching
The --no-storage-caching flag disables Anvil’s RPC storage caching. With this flag, storage slots are read directly from the remote endpoint on every execution.
That option has a narrow use case. It can help diagnose cache-related behavior or force fresh reads during a controlled investigation. It is usually hostile to fast, repeatable fork testing.
The flag does not mean that all fork behavior is identical to a fully uncached node in every respect. It specifically disables RPC storage caching. Other local execution behavior remains local. The relevant consequence is that repeated storage reads lose the local reuse path.
A test that relies heavily on storage reads will expose the difference immediately. Calls into token balances, pool reserves, lending positions, and proxy storage become dependent on upstream response time for each uncached read.
Do not confuse --cache-path with the RPC fork cache
Anvil has options related to persisted node states. Those options are separate from the RPC state cache.
The --cache-path option concerns persisted node states used with features such as --max-persisted-states. It does not relocate the fork RPC cache that contains storage.json.
This distinction prevents a common operational error. Changing the persisted-state path does not move the remote-state cache. The fork RPC cache remains under the Foundry RPC cache directory unless the relevant tooling behavior changes in a version-specific way.
The two mechanisms answer different questions:
| Mechanism | Stores | Primary use | Affects remote RPC reads |
|---|---|---|---|
| RPC fork cache | Fetched account data, contract code, and storage slots | Reuse remote state across fork executions | Yes |
| Persisted node state | Local Anvil state snapshots | Restore a local node state | Indirectly |
| In-memory local state | Current process mutations | Execute the active test session | No remote write |
| Upstream RPC provider | Canonical chain data | Supply missing fork state | Source of uncached reads |
The RPC cache is a read cache for fork construction. A persisted node state is a snapshot of local execution. They can be used in the same workflow, but they are not interchangeable.
storage.json is not a snapshot of your entire fork. It is a record of remote state fetched for the fork and retained for later resolution.Offline-start and chain ID pinning
Anvil can start a fork without fetching the chain ID from the remote node when the command includes an explicit chain identifier:
anvil --fork-url <RPC_URL> --fork-block-number <BLOCK> --fork-chain-id <CHAIN_ID>
The --fork-chain-id value tells Anvil which chain identity to use. This allows an offline-start mode when the required fork state is already cached locally on disk.
The sequence is strict:
1. The fork must be identified by the expected chain and block.
2. The cache must already contain the state required during startup and test execution.
3. The chain ID must be supplied explicitly.
4. Anvil can avoid fetching the chain ID from the upstream endpoint.
This does not turn an incomplete cache into a complete chain. If execution touches a state item that is absent locally, Anvil still needs the upstream provider unless another local source supplies that state.
Offline start is therefore useful after a warm-up run. The first run populates the cache. Later runs can start with reduced network dependency, provided they access only cached state.
Why explicit chain identity matters
The chain ID is part of the execution environment. It influences transaction signing and protects against replay across networks. A fork configured with the wrong chain ID can produce a test environment that accepts signatures or transaction assumptions inconsistent with the intended network.
The value must match the chain being forked. The flag is not a performance-only setting. It is a correctness parameter.
The same applies to the fork block. A cache entry associated with one chain and block should not be treated as a valid base for another chain or block. The local test environment must preserve the identity of its upstream state.
Warm-up runs and deterministic test setup
A controlled workflow separates cache population from test execution.
During the warm-up phase, run the suite with network access and the intended fork parameters. Allow Anvil to fetch the required state. Then execute the suite again using the same chain and block.
The second run should reuse cached account data, contract code, and storage slots. If it still produces substantial remote traffic, investigate the cause:
- The fork block changed.
- The chain identity changed.
- The cache directory is not available to the process.
- The suite touches new state on each run.
- Storage caching was disabled.
- The test starts from a different fork configuration.
- The cache was removed between runs.
The diagnostic method is straightforward. Compare the fork command, inspect the cache path, and identify which state reads are actually stable. Do not infer cache failure from a test that intentionally creates new addresses or reaches new protocol branches.
Forked state versus persistent node snapshots
Fork caching answers the question of how Anvil obtains remote state. Persistent snapshots answer the question of how Anvil restores local state.
These systems operate at different points in the lifecycle.
A fork cache may contain a contract’s code and storage slots fetched from an upstream chain. A local snapshot may contain the result of test transactions applied after the fork began. For example, a test can start from a block-pinned fork, deploy a mock oracle, change a local token balance, and persist the resulting node state. Restoring that snapshot returns the local mutations. It does not imply that the upstream chain was modified.
This distinction is critical in test design. A snapshot can restore a post-setup environment quickly. The RPC cache can prevent repeated retrieval of the base state used to construct that environment.
The execution layers can be summarized as follows:
- Fork parameters define the chain and block.
- RPC cache supplies previously fetched remote values.
- Local fork database tracks resolved state.
- Test transactions mutate the local state.
- Snapshots preserve selected local states for restoration.
A snapshot does not replace fork pinning. Restoring a snapshot created from an ambiguous latest-block fork still restores an environment with an unstable upstream origin. The snapshot may be internally consistent, but its provenance remains weak.
For production-grade test infrastructure, the inputs should be explicit:
1. Pin the chain.
2. Pin the fork block.
3. Use a stable RPC endpoint for cache population.
4. Preserve the Foundry RPC cache between compatible runs.
5. Keep local node snapshots separate from remote-state cache management.
6. Disable storage caching only for a defined diagnostic reason.
7. Record the exact Anvil arguments used by CI and local development.
The support structure around a test environment matters as much as the command itself; even outside blockchain engineering, the broader idea appears in profiles such as The Support System Behind Learner Tien: Family, Coaching, and Early Career Development. The analogy ends there. In Anvil, the relevant support system is deterministic state resolution, not narrative context.
A security and correctness audit of the workflow
RPC caching improves execution cost. It does not prove that the test is valid.
A cached value can still be the wrong value if the fork block is wrong. A locally mutated slot can conceal a contract bug if setup code changes state before the vulnerable path is reached. A snapshot can make a test pass while hiding an incomplete initialization sequence.
The cache should therefore be treated as an optimization layer, not an authority.
A practical audit focuses on invariants:
- The chain ID must match the intended upstream network.
- The fork block must be explicit for tests that depend on historical state.
- Contract code must resolve from the expected fork.
- Token balances used in setup must be local test mutations, not assumptions about remote writes.
- Storage caching must not be disabled accidentally in CI.
- Repeated runs must use the same cache identity when comparing performance.
- A test must fail if a required remote state read is unavailable in an intended offline mode.
The last invariant is useful. Offline-start testing exposes hidden dependencies. If a suite claims to be reproducible from a warm cache but fails because it silently requires a new remote slot, the failure identifies an undeclared input.
That is a better result than allowing the suite to contact the provider unpredictably.
The operational result
Anvil state forking is lazy by design. It resolves only the remote state that execution touches. RPC caching makes that laziness reusable.
The first run pays the cost of fetching uncached state. Subsequent runs can read the same account data, bytecode, and storage slots from the local cache. This reduces network round-trips, limits provider consumption, and removes a major source of test latency.
The boundaries are exact:
- Fork RPC caching is stored by default under
~/.foundry/cache/rpc/<chain>/<block>/storage.json. --no-storage-cachingforces storage reads back to the upstream endpoint.--fork-chain-idcan support offline start when the required state is already cached.--cache-pathconcerns persisted node states, not the RPC fork cache.- Local writes remain local. They never mutate the remote chain.
- Performance gains depend on state reuse. There is no universal duration reduction.
The secure configuration is not complicated. It is strict.
Pin the fork block. Pin the chain ID. Persist the correct cache. Separate remote-state caching from local snapshots. Treat every uncached request as an external dependency. Treat every local state mutation as a test artifact.
That is the whole mechanism. Anvil does not make remote state disappear. It makes repeated access deterministic and local.
FAQ
Why are Anvil fork tests slow?
How does RPC caching speed up Anvil fork tests?
Where does Anvil store the RPC fork cache?
What does the --no-storage-caching flag do?
Can Anvil start a fork offline?
By Caleb North