Aggregate N Ethereum contract calls into far fewer HTTP round-trips using Multicall3 and Alloy.
N calls
├─ chunked into groups of chunk_size → fewer, larger eth_calls
└─ up to max_concurrent chunks per wave → parallel HTTP requests
5 000 decimals() calls against 1 000 unique addresses, publicnode.com,
10 runs averaged. All approaches use parallel execution:
| Approach | HTTP reqs | Avg time | vs fastest |
|---|---|---|---|
Concurrent eth_call (join_all) |
5 000 | ~16 000 ms | 4.3× slower |
| Alloy Multicall3 (parallel chunks) | 25 | ~4 200 ms | 1.1× slower |
| multicall-batcher | 25 | ~3 700 ms | fastest |
chunk_size=200, max_concurrent=5.
Run it yourself: cargo run --release --example benchmark
[dependencies]
multicall-batcher = { path = "." }
alloy = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }use alloy::{primitives::address, sol, sol_types::SolCall};
use multicall_batcher::{Call, MulticallBuilder};
sol! {
interface IERC20 {
function balanceOf(address owner) external view returns (uint256);
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let mc = MulticallBuilder::new()
.chunk_size(200)
.max_concurrent(5)
.connect("https://ethereum.publicnode.com")
.await?;
let usdc = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
let holders = vec![ /* ... */ ];
let calls: Vec<Call> = holders.iter().map(|&owner| {
Call::new(usdc, IERC20::balanceOfCall { owner }.abi_encode())
}).collect();
let results = mc.call(calls).await?;
for r in &results {
if r.success {
let bal = alloy::primitives::U256::from_be_slice(&r.return_data[..32]);
println!("{bal}");
}
}
Ok(())
}| Method | Default | Description |
|---|---|---|
.chunk_size(n) |
100 | Calls per aggregate3. Keep ≤ 200 for public RPCs. |
.max_concurrent(n) |
5 | Max in-flight aggregate3 calls. |
.multicall3(addr) |
canonical | Override for testnets / custom deployments. |
.connect(url) |
— | Connect and return BatchedMulticall. |
Call::new(target, calldata) // allow_failure = true (recommended)
Call::strict(target, calldata) // allow_failure = false (reverts whole batch)pub struct CallResult {
pub success: bool,
pub return_data: Bytes,
}0xcA11bde05977b3631167028862bE2a173976CA11 — same on Ethereum, Arbitrum,
Base, Optimism, Polygon, and 50+ chains.
cargo run --example erc20_balances # 6 balanceOf calls in 1 round-trip
cargo run --release --example benchmark # full benchmark
cargo run --release --example benchmark_proxy # proxy throughput testLanguage-agnostic JSON API:
cargo run --bin server -- --rpc https://ethereum.publicnode.com --port 3000curl -X POST http://localhost:3000/call \
-H "Content-Type: application/json" \
-d '[{"target":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","calldata":"0x313ce567"}]'Drop-in replacement — intercepts eth_call, batches via Multicall3, forwards
everything else:
cargo run --bin proxy -- --rpc https://ethereum.publicnode.com --port 8545MIT