eth_createAccessList simulates a call against current state and returns the addresses and storage keys that call would touch, plus a gasUsed figure. You can attach that list to a type-1 (EIP-2930) or type-2 (EIP-1559) transaction so the first touch of each slot is paid at the declared access-list price instead of the higher cold-access price defined by EIP-2929. The tradeoff is real: access lists add intrinsic gas per address and per storage key plus calldata bytes, so they only pay off when warmed slots are reused enough times. The only reliable way to know is to compare eth_estimateGas with and without the list at the same block. This guide covers the request/response envelope, a runnable Node.js example, a results table you fill against your own endpoint, and the limitations that make access lists a niche optimization rather than a default.
The EIP-2929 Cold and Warm Access Model
Before EIP-2930 access lists make sense, you need the accounting change that created the opportunity. EIP-2929 redefined state access costs by splitting every address and storage slot into two states: cold (not yet touched in this transaction) and warm (already touched). The first access to an address or slot in a transaction is cold and costs substantially more gas than the warm accesses that follow.
This is why declaring accessed slots up front can lower cost. If a transaction will touch the same storage slot several times, or touch a set of slots that the EVM would otherwise charge cold prices for, pre-warming them through an access list moves those first touches to the cheaper access-list price. The mechanism is purely about ordering and declaration: the EVM still performs the same reads and writes, but the cold surcharge is paid once at a known, lower rate.
The practical consequence is that access lists are a targeted optimization for contracts with predictable, repeated storage access patterns. They do nothing for a simple value transfer, because a plain transfer touches no contract storage slots that would benefit.
- Cold access: first touch of an address or storage slot in a transaction, charged at the higher EIP-2929 rate.
- Warm access: any subsequent touch in the same transaction, charged at the lower rate.
- Access lists pre-warm declared addresses and slots so their first touch is billed at the access-list price.
- The benefit scales with how many times the warmed slots are actually reused.
What an EIP-2930 Access List Actually Is
EIP-2930 introduced the type-1 transaction and the accessList field: a list of objects, each with an address and an array of storageKeys. Declaring a slot in that list tells the EVM to treat it as warm from the start of execution. Type-2 (EIP-1559) transactions also carry an accessList field, so you are not forced onto the legacy type-1 fee model just to use access lists.
The field is optional on both transaction types. When omitted or empty, the transaction behaves exactly as it did before EIP-2930. When present, the client pays intrinsic gas for each declared address and each declared storage key, plus the calldata cost of encoding the list. That intrinsic cost is the reason a list can be net-negative: you pay up front for warmth you may not fully use.
If you want to inspect how these fields are serialized on the wire, the raw transaction encoding is the same family covered in eth_getRawTransactionByHash and type-0/1/2 encodings. The accessList is part of the RLP payload for type-1 and type-2 transactions.
- Shape: accessList is an array of { address, storageKeys[] } objects.
- Type-1 transactions are the original EIP-2930 carrier; type-2 transactions can also include the field.
- Intrinsic cost: per declared address, per declared storage key, plus calldata bytes.
- An empty or omitted list is valid and equivalent to pre-EIP-2930 behavior.
The eth_createAccessList Request and Response Envelope
eth_createAccessList is a simulation method. You send it a call object and a block tag, and it returns the access list that call would need plus a gasUsed estimate. The Ethereum JSON-RPC specification documents the request and response shape. The call object accepts from, to, data, and value, and the second parameter is the block tag (for example "latest" or a hex block number).
The response is an object with two fields: accessList, an array of { address, storageKeys[] } entries, and gasUsed, a hex quantity. The accessList is exactly what you would paste into a transaction's accessList field. The gasUsed reflects the simulated execution, not a guarantee of the final mined cost, because state can change between simulation and inclusion.
Because this is a simulation against a specific block, the result is only valid at that block. If the contract's storage layout or the touched slots change, the list may be stale. Treat the returned list as a starting point to measure, not a permanent artifact.
- Params: [callObject, blockTag] where callObject supports from, to, data, value.
- Result: { accessList: [{ address, storageKeys[] }], gasUsed: hex }.
- The returned accessList is directly usable as a transaction field.
- Validity is tied to the block the simulation ran against.
Runnable Node.js Example: Create, Estimate, Compare
The example below uses raw JSON-RPC over fetch so you can see the exact envelopes. It calls eth_createAccessList for a contract call, prints the accessList and gasUsed, then calls eth_estimateGas twice at the same block: once without the list and once with it. The delta is the number that matters.
Replace the RPC_URL with your endpoint. OnFinality exposes Ethereum JSON-RPC through the Ethereum network page; the same method is available on any node that implements it, though support varies by provider. The example deliberately does not assert a savings figure, because the result depends on the contract and the block.
const RPC_URL = "https://your-endpoint.example";
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
async function main() {
const call = {
from: "0xYourFromAddress",
to: "0xContractAddress",
data: "0xYourCalldata"
};
const blockTag = "latest";
const created = await rpc("eth_createAccessList", [call, blockTag]);
console.log("accessList:", JSON.stringify(created.accessList, null, 2));
console.log("gasUsed (createAccessList):", created.gasUsed);
const withoutList = await rpc("eth_estimateGas", [call, blockTag]);
const withList = await rpc("eth_estimateGas", [
{ ...call, accessList: created.accessList },
blockTag
]);
const delta = BigInt(withList) - BigInt(withoutList);
console.log("estimateGas without list:", withoutList);
console.log("estimateGas with list: ", withList);
console.log("net delta (negative = saving):", delta.toString());
}
main().catch((e) => { console.error(e); process.exit(1); });Folding the Result into a Type-1 or Type-2 Transaction
Once you have the accessList, attaching it is mechanical. For a type-1 transaction, set type to 0x1 and include the accessList field alongside gasPrice, gasLimit, nonce, to, value, and data. For a type-2 transaction, set type to 0x2 and include accessList alongside maxFeePerGas and maxPriorityFeePerGas. The accessList field is identical in both cases.
If you use ethers, the accessList is passed through the transaction request object and the library handles serialization. If you build raw transactions, the accessList is RLP-encoded as part of the type-1 or type-2 payload. The encoding details are the same family described in eth_getRawTransactionByHash and type-0/1/2 encodings.
The important discipline is to verify the net effect before broadcasting. Attaching a list that does not pay for itself makes the transaction strictly more expensive. Always compare estimates at the same block, as the example does.
- Type-1: type 0x1, gasPrice, accessList.
- Type-2: type 0x2, maxFeePerGas, maxPriorityFeePerGas, accessList.
- The accessList field shape is the same for both types.
- Verify with eth_estimateGas before broadcasting.
The Cost Tradeoff: Intrinsic Gas Versus Warmth
An access list is not free. You pay intrinsic gas for every declared address and every declared storage key, plus the calldata cost of encoding the list. That cost is incurred whether or not the execution actually reuses the warmed slots. The saving only materializes when the warmed slots are touched enough times that the avoided cold surcharges exceed the intrinsic cost.
This is why the net saving can be negative. A list that declares many slots but only touches a few of them once will lose gas. A list that declares a small set of hot slots touched repeatedly can win. The crossover point depends on the contract, the calldata, and the block, so it must be measured rather than assumed.
For fee-market context around the estimate, the base-fee and priority-fee mechanics are covered in Estimating gas price with eth_feeHistory. Access lists change the execution gas component, not the fee-market component, so the two analyses are complementary.
- Cost side: per-address intrinsic gas, per-storage-key intrinsic gas, calldata bytes.
- Benefit side: avoided cold surcharges on slots that are actually reused.
- Net saving can be positive, zero, or negative depending on reuse.
- Measure at the same block with and without the list.
Reproducible Measurement: A Results Table You Fill
Because access-list savings are contract- and block-specific, the honest approach is a measurement table you populate against your own endpoint. Run the Node.js example above for each call you care about, record the values, and compute the net delta. Do not copy numbers from a blog post, including this one; the numbers depend on state that changes.
Use the same block tag for both estimates in a row so the comparison is fair. If you want to check stability, repeat the row at a later block and note whether the delta moved. A delta that flips sign across blocks is a sign the optimization is not robust for that call.
- Call: a short label for the contract call you tested.
- Chain id: the network you ran against.
- gasUsed from eth_createAccessList: the simulated execution gas.
- eth_estimateGas without list: the baseline estimate.
- eth_estimateGas with list: the estimate with the returned accessList attached.
- Net delta: with-list minus without-list; negative means a saving.
- Decision: attach the list, skip it, or re-measure at a later block.
Limitations and When Access Lists Do Not Help
The first limitation is method support. eth_createAccessList requires the node to implement it, and support is documented / varies by provider. If your endpoint returns a method-not-found error, you cannot use this workflow without switching endpoints. The Ethereum RPC node guide (RPC Assistant) is a useful reference for checking what a node exposes.
The second limitation is staleness. The result is only valid at the block it was simulated against, because state and warm/cold status drift as new blocks arrive and as other transactions change storage. A list generated at block N may be suboptimal or wrong at block N+1.
The third limitation is scope. Access lists rarely help simple transfers, because a plain value transfer touches no contract storage slots that would benefit from pre-warming. They are also not a universal win on L2s, where the fee model differs and the saving may not transfer. Finally, the estimate itself is a simulation; the mined cost can differ if execution paths diverge.
- Method support: documented / varies by provider.
- Staleness: valid only at the simulated block.
- Scope: rarely helps simple transfers.
- L2 fee models differ; savings may not transfer.
- Simulation is not a guarantee of mined cost.
Troubleshooting: Method Not Found, Empty Lists, Negative Savings
Method not found is the most common failure. It means the node does not implement eth_createAccessList. Check the provider's method list; if it is absent, you cannot generate lists this way. Some providers expose it only on archive or debug-enabled endpoints, so the same provider may behave differently across plans.
An always-empty accessList usually means the call touches no contract storage slots that the method considers worth declaring, or that the call reverts or is trivial. Verify the call object is correct (to, data, from) and that the block tag is valid. If the call reverts, the simulation may return an empty list rather than an error.
A negative saving means the intrinsic cost of the list exceeded the avoided cold surcharges. This is a legitimate result, not a bug. Reduce the list to only the slots that are actually reused, or skip the list entirely. Re-measure at the same block to confirm the delta is stable before deciding.
- Method not found: node does not implement the method; check provider docs.
- Empty list: call may be trivial, revert, or touch no relevant slots.
- Negative saving: list costs more than it saves; trim or skip.
- Always re-measure at the same block before deciding.
Next Steps: Estimating, Tracing, and Choosing an Endpoint
Access lists are one lever among several for controlling gas. The estimate itself is covered in eth_estimateGas: gas limits and slippage, which explains how to set a gas limit with headroom. The fee-market side is covered in Estimating gas price with eth_feeHistory. Together they give you the full picture around a transaction's cost.
If you need to understand why a call touches the slots it does, tracing is the next tool. Ethereum transaction tracing with trace and debug shows how to inspect execution at the opcode level, which is useful when an access list behaves unexpectedly.
For endpoint selection, the Ethereum network page lists the network, and RPC pricing and the API service describe how access is provisioned. The OnFinality Learn hub collects the rest of the guides in this series. Start by running the measurement table against your own endpoint before adopting access lists in production.
- Estimate gas limits with headroom: eth_estimateGas guide.
- Understand fee-market percentiles: eth_feeHistory guide.
- Inspect execution: trace and debug guide.
- Choose an endpoint: network page, pricing, and API service.