Multicall3 Cheat Sheet: One `eth_call` to Rule Them All

June 4, 2026 · 7 min read · #ethereum #multicall #json-rpc #tutorial

You can do 50 contract reads as 50 eth_call requests. With JSON-RPC batching you can do them in one HTTP request. With Multicall3 you can do them in one eth_call — meaning the chain itself executes all 50 reads in a single atomic snapshot, and you pay one round-trip.

That last step is the difference between 200ms and 80ms on the same network. It's also the difference between "all 50 reads see the same block" and "some of these reads might be one block apart from the others." For anything time-sensitive — DEX pricing, NFT inventory, lending position health — that atomicity matters.

This post is what Multicall3 actually is, the four function variants and when to use each, working code in viem and ethers, and the gotchas that catch people once or twice before they get it right.

What Multicall3 actually is

A single deployed contract at the same address on every EVM chain:

0xcA11bde05977b3631167028862bE2a173976CA11

It's deployed via deterministic CREATE2 deployment, which is why the address is identical on Ethereum, Base, Arbitrum, Polygon, Optimism, BSC, Avalanche, Linea, Scroll, Blast, Mantle, and essentially every other EVM chain you'd care about. Project lives on GitHub at mds1/multicall.

The contract has four interesting functions. The simplest is aggregate:

function aggregate(Call[] calldata calls)
  external
  returns (uint256 blockNumber, bytes[] memory returnData);

struct Call {
  address target;
  bytes callData;
}

You hand it an array of (contract address, encoded call data) tuples. It loops through, calls each one, and returns all results as raw bytes. Plus the block number at which they were all read — that's the atomicity guarantee.

The four variants

Different functions for different failure modes. Pick by which one suits the workload.

aggregate — all-or-nothing

If any individual call reverts, the whole batch reverts. Use this when every call needs to succeed for the result to be meaningful.

tryAggregate(requireSuccess, calls) — soft mode with a global flag

If requireSuccess = false, individual call failures don't revert the batch. Each result comes back as (bool success, bytes returnData).

function tryAggregate(bool requireSuccess, Call[] calldata calls)
  external
  returns (Result[] memory returnData);

struct Result {
  bool success;
  bytes returnData;
}

Use this when you're querying many contracts and some are expected to fail (e.g., reading from contracts that might not be deployed yet, or that might revert on some inputs).

aggregate3(calls) — per-call allowFailure

This is the one to default to. Each call specifies its own allowFailure flag, so you can mix strict and soft calls in the same batch:

function aggregate3(Call3[] calldata calls)
  external
  returns (Result[] memory returnData);

struct Call3 {
  address target;
  bool allowFailure;
  bytes callData;
}

Better than tryAggregate because it lets you require one specific read to succeed (say, an oracle price) while allowing optional reads to fail (say, individual token balance queries that might error on weird tokens).

aggregate3Value — same but with ETH value attached

Use this when calls need to send native value (rare for reads, common for writes). Most read-only multicall workflows never need it.

Worked example — viem

viem has Multicall3 support built in. You don't construct the call manually:

import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http("https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY"),
});

const erc20Abi = parseAbi([
  "function balanceOf(address) view returns (uint256)",
]);

const tokens = [USDC, USDT, DAI, WBTC, WETH];
const user = "0xabc...";

// viem handles all the encoding/decoding/multicall plumbing
const balances = await client.multicall({
  contracts: tokens.map(token => ({
    address: token,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [user],
  })),
  allowFailure: true,  // uses aggregate3 under the hood
});

balances.forEach((r, i) => {
  if (r.status === "success") {
    console.log(`${tokens[i]}: ${r.result}`);
  } else {
    console.log(`${tokens[i]}: failed - ${r.error.message}`);
  }
});

One HTTP request, one RPC call, five token balances. With allowFailure: true, a single token contract reverting doesn't kill the whole query.

Worked example — ethers v6

ethers doesn't have built-in multicall. Use ethers-multicall-provider:

import { JsonRpcProvider, Contract } from "ethers";
import { MulticallWrapper } from "ethers-multicall-provider";

const rawProvider = new JsonRpcProvider(
  "https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY"
);
const provider = MulticallWrapper.wrap(rawProvider);

const erc20Abi = ["function balanceOf(address) view returns (uint256)"];

const balances = await Promise.all(
  tokens.map(token =>
    new Contract(token, erc20Abi, provider).balanceOf(user)
  )
);

The wrapper coalesces Promise.all calls into a single multicall under the hood. Same outcome as viem's explicit multicall().

Worked example — web3.py

Python doesn't have an idiomatic multicall library quite as polished as viem's, but multicall.py works:

from web3 import Web3
from multicall import Multicall, Call

w3 = Web3(Web3.HTTPProvider("https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY"))

def balance_of(token, user):
    return Call(token, ["balanceOf(address)(uint256)", user], [(token, None)])

multi = Multicall([balance_of(t, USER) for t in TOKENS], _w3=w3)
results = multi()
# results is a dict of {token_address: balance}

The actual measurement

We benchmarked 50 ERC-20 balanceOf reads against the same Ethereum endpoint from a US-East client:

Pattern Wall-clock
Sequential eth_call × 50 ~2,800 ms
Promise.all without batching ~280 ms
Promise.all with auto-batching ~120 ms
Single Multicall3 eth_call ~85 ms

The multicall step is another 30% faster than even auto-batched parallel calls — because the chain itself iterates through them, no network round-trips between calls, and one node-side execution path instead of 50.

For larger batches the gap widens. At 500 reads, sequential is unusable (~30 seconds), batched parallel is ~600ms, multicall is ~200ms.

Gotchas — the ones that bite people

1. Gas limit applies even to view calls

eth_call runs with a gas limit, usually 50M on most providers. A multicall with 500 sub-calls each costing 10k gas = 5M gas, fine. A multicall with 100 sub-calls each costing 500k gas = 50M, fails. Heavy contract reads can hit this faster than you'd expect.

Mitigation: split into multiple multicall requests of ~50-100 calls each. The atomicity guarantee is per-multicall, so the splits will be at different block numbers — usually within 1-2 blocks of each other, which is fine for most use cases.

2. Return data is bytes — you have to decode

The contract returns raw bytes. viem and ethers-multicall-provider decode automatically using the ABI. If you call multicall manually (rare), you need to use abi.decode or defaultAbiCoder.decode on each result.

3. Block number matters

The returned blockNumber from aggregate is the block at which the multicall executed. All sub-calls saw the same block state. If you save this number and use it later (e.g., to verify prices against an oracle), make sure your code paths are consistent about which block the snapshot reflects.

4. Not all contracts can be multicalled safely

If a contract's return value depends on block.timestamp or msg.sender, the multicall result reflects the multicall transaction's view of these — block.timestamp of the multicall block, and msg.sender = the Multicall3 contract itself, not your address. For pure reads that depend only on storage state, this is fine. For contracts that gate reads by msg.sender, multicall doesn't work.

5. State-changing calls require a transaction

Multicall is eth_call for reads, but you can also send a transaction to it for atomic batch writes. The address is the same. If you want to perform multiple state changes atomically (e.g., approve + swap in one tx), Multicall3 supports that — just send a transaction with aggregate3 instead of using eth_call.

That said, most DEXes have their own batch routers (Uniswap V4 Universal Router, etc.) that are better-optimized for specific multi-step transactions. Multicall3 is the generic tool; per-protocol routers are usually faster for specific protocols.

When NOT to use Multicall3

A few cases where multicall is the wrong answer:

  • One read: obvious — overhead isn't worth it.
  • State that depends on msg.sender: see gotcha #4. The Multicall3 contract is the caller, so msg.sender-gated reads don't work.
  • Cross-block reads: you want different snapshots for some calls (e.g., one read at current, one at a historical block). Multicall is atomic — all calls at one block.
  • Heavily gas-intensive single calls: a single contract read consuming 30M gas leaves no headroom for batching anyway.

The takeaway

Multicall3 is the single biggest performance win available for any read-heavy EVM workload. Same address on every chain, same interface, supported by every major client library. Use aggregate3 with allowFailure per call as the default — it's the most flexible variant and handles real-world "some of these might fail" workloads cleanly.

For most apps doing 5+ contract reads at the same block, Multicall3 should be the default pattern. Sequential eth_calls should look weird and need justification in code review.


SwiftNodes provides Multicall3-enabled RPC on every supported chain. Same ?key= URL works for plain eth_calls and Multicall3 invocations. See Ethereum RPC details, Base RPC, or grab a free API key — no credit card, no KYC.

Related posts

  • How `eth_getLogs` Range Caps Bite You in Production

    Your indexer works fine until it hits a single dense block range, then everything stops. Here's the cap mechanics across providers, the adaptive chunking pattern that actually works, and the code to do it right.

  • JSON-RPC Batching: The 100× Speedup Most Devs Skip

    Most dApps make 50 RPC calls when they could make one. Here's how JSON-RPC batching works, why ethers.js and viem already do most of it for you, and where you still have to think about it.

  • Nonce Management for High-Throughput Senders

    One stuck nonce can freeze every transaction behind it. If you're sending from a hot wallet at volume — a relayer, a market maker, a bridge — leaning on eth_getTransactionCount('pending') will eventually stall you. Here's how nonces actually work and how to manage them locally at scale.

Try SwiftNodes free — multi-chain RPC across 75+ networks, flat-rate pricing, pay by card or crypto, no KYC. Get an API key in 30 seconds →