blockchainsv
Web3 Integration & APIs·August 16, 2026·14 min read

Viem multicall batching: why aggregated reads save RPC limits

The failed invariant is simple: a frontend reads several values from the same protocol state, but each RPC request may execute against a different block.

Viem multicall batching: why aggregated reads save RPC limits

The interface then combines data that never existed together on-chain.

A token balance can come from block N. The allowance can come from N+1. The protocol configuration can come from N+2. Each result is individually valid. The combined state is not deterministic.

Viem multicall batching addresses both parts of the problem. It aggregates read-only contract calls into one eth_call, reduces HTTP round trips, and returns the batch from a single block snapshot. It is not a cosmetic optimization. It changes the consistency model of the read path.

The distinction matters. A frontend that performs ten independent readContract calls creates ten separate RPC operations. A frontend that uses publicClient.multicall sends one call to the Multicall3 contract, which executes the sub-calls against the same state.

The attack surface begins with inconsistent reads

Consider a swap interface. It needs to read:

  • the user’s token balance;
  • the user’s allowance;
  • the pool reserves;
  • the token decimals;
  • the router configuration;
  • the current protocol fee.

If those reads are issued independently, the provider may process them across multiple blocks. The chain can advance between requests. A reserve update can land between the first and fifth call. An allowance transaction can be mined while the frontend is still assembling its view.

This does not automatically create a protocol exploit. The contract remains the source of truth. But it creates a broken frontend invariant:

Every value used to construct one user decision should describe the same chain state.

The practical symptoms are familiar:

  • a button is enabled using a balance that is already stale;
  • a quote is rendered against reserves from a different block;
  • an allowance appears insufficient even though the approval transaction has landed;
  • a dashboard shows totals that cannot be reproduced from any single block;
  • a transaction is prepared from parameters assembled across inconsistent reads.

A second request does not repair the first request’s block context. Retrying introduces another state transition. Polling can reduce staleness, but it does not make a set of independent calls atomic.

The correct primitive is a single read operation with multiple sub-calls.

What publicClient.multicall actually does

Viem provides a native publicClient.multicall action. It accepts an array of contract calls and sends them through Multicall3. The Multicall3 contract executes the calls and returns their results in the original order.

The main Multicall3 method is aggregate3(Call3[] calls). Each item contains a target contract, calldata, and an allowFailure flag. The result records whether the individual sub-call succeeded and returns the returned bytes.

Multicall3 is deployed at the deterministic address 0xcA11bde05977b3631167028862bE2a173976CA11 across more than 70 EVM-compatible networks. Viem uses the deployment configured for the selected chain. The address is not a reason to hardcode your own routing logic. Chain configuration remains the control point.

A typical Viem call is expressed through publicClient.multicall({ contracts: [...] }). Each contract entry specifies an address, ABI, and function name. Arguments are supplied through args when required. Viem handles calldata encoding and return-value decoding.

The resulting array preserves call order. If the first contract in the input array reads a balance, the first result corresponds to that balance. This ordering is useful, but it is not sufficient for correctness. The result status must also be inspected.

With allowFailure enabled, one failed sub-call does not necessarily revert the complete batch. Viem defaults allowFailure to true. That behavior is powered by Multicall3’s aggregate3 method. A missing optional feed, a reverted view function, or an ABI mismatch can therefore produce a partial result rather than a total failure.

That is operationally useful. It is also a possible source of silent corruption.

A frontend must distinguish at least three conditions:

1. the batch request failed at the RPC or transport layer;

2. the batch executed, but one sub-call failed;

3. the batch executed and every required sub-call succeeded.

Treating all three as “data loaded” is a state-management defect.

Multicall gives you one block snapshot. It does not guarantee that every sub-call succeeded. Snapshot consistency and result validity are separate invariants.

Why one eth_call is different from JSON-RPC batching

The terminology is frequently abused. JSON-RPC batching and Multicall batching are not the same operation.

JSON-RPC batching packages several JSON-RPC requests into one HTTP payload. The provider receives multiple independent calls. The calls are not transformed into one EVM execution. They may be counted separately against provider quotas. They may also observe different blocks, depending on provider behavior and timing.

Multicall batching encodes several contract calls into one call to the Multicall3 contract. The provider receives one eth_call. The EVM executes the aggregate operation against one state snapshot. This is the relevant distinction for both request overhead and consistency.

The model is easier to audit when expressed as execution layers:

LayerWhat is combinedState snapshotRPC accounting
Independent readsNothingPotentially different block per requestOne RPC operation per read
JSON-RPC batchHTTP request envelopeNot inherently one blockRequests may count separately
Multicall3EVM contract callsOne snapshot for the aggregate callOne eth_call, subject to provider limits

The aggregate call still has a payload. It still consumes provider resources. “One request” does not mean “free” or “unbounded.” The provider evaluates calldata size, execution cost, response size, and account-level limits.

The relevant optimization is fewer transport operations and a shared read context. It is not the elimination of computation.

The shape of a production-grade Viem batch

A useful batch is built around one UI decision or one domain query. It is not a random collection of every read the application might need.

For a lending position, a coherent batch could include the account’s supplied balance, borrowed balance, health-factor inputs, collateral configuration, and current oracle value. For an NFT page, it could include ownership, token URI, sale state, and collection-level configuration. For a wallet header, it might include native balance, selected ERC-20 balances, and allowance data for the active router.

The grouping rule is strict:

  • combine values that must describe one state;
  • avoid combining unrelated screens merely because they use the same client;
  • keep optional calls separate from mandatory calls when failure semantics differ;
  • preserve a stable input order;
  • identify each result by a semantic label in application code.

The last point matters because arrays are positional. A contract added at index two can shift every later result. The code may continue to compile. The UI can then display a valid decoded value under the wrong label.

A safer application structure defines a named mapping around the returned array. The mapping should validate the expected result count and check the status field for every required result. Do not infer success from the presence of a result property alone. A failed sub-call can still produce a structured response.

ABI precision is another hard boundary. Multicall does not repair a wrong ABI. If the function signature is wrong, Viem encodes the wrong selector or decodes the return bytes incorrectly. A batch can succeed at the aggregate layer while one entry fails or returns data that the application misinterprets.

The execution path is deterministic only when these inputs are deterministic:

  • chain ID;
  • contract address;
  • ABI;
  • function name;
  • argument types and values;
  • block context;
  • result handling policy.

A chain switch must invalidate the batch. So must an account change when the calls use an address argument. Otherwise the frontend can render a valid response for the previous chain or account.

allowFailure is a policy decision

The default allowFailure: true is appropriate for many dashboards. It is not automatically correct for financial actions.

Classify calls before batching them.

Required reads are values without which the interface must not prepare or enable an action. A missing exchange rate, reserve value, or allowance should block the dependent operation.

Optional reads improve the interface but do not define its safety. A secondary metadata call can fail without invalidating the primary state.

Independent reads belong in separate batches if a failure in one domain should not affect another domain’s loading state.

For a required group, setting allowFailure: false can be the correct policy. One failed sub-call then causes the aggregate operation to revert instead of returning a partially valid set. This reduces ambiguity. It also means one defective or unavailable call blocks all other results in that batch.

The alternative is allowFailure: true with explicit per-result validation. That gives finer control. It also increases the amount of state logic that must be reviewed.

The failure mode must be visible in the interface. Showing a zero value is not a safe fallback for a failed balance read. Showing an old oracle value without marking it stale is worse. The contract call failed; the application should preserve that fact.

Batch size is an RPC limit, not a theoretical detail

Viem exposes a batchSize parameter for multicall. The documented default is 1024 bytes of calldata per chunk, or the value configured through the client’s batch settings. Large batches are split to avoid exceeding provider calldata or execution limits.

This setting controls chunking. It does not guarantee that every provider accepts every resulting request. Providers apply their own HTTP payload, compute-unit, response, timeout, and rate-limit rules. There is no universal maximum that can be assumed across Infura, Alchemy, QuickNode, self-hosted nodes, and chain-specific gateways.

The consequence is direct: multicall reduces request count, but a single oversized aggregate can still fail.

A batch with many calls can hit limits through:

  • long dynamic arguments;
  • large return values;
  • arrays returned from view functions;
  • storage-heavy contract execution;
  • a high number of sub-calls;
  • provider-specific response or compute limits.

The 1024-byte default is a defensive chunk size, not a promise about total throughput. If a client is configured with a different batch size, the effective behavior changes. The application should treat the client configuration as part of its deployment environment.

Do not increase batchSize merely because fewer requests look better in a network panel. The optimization target is stable latency under provider constraints. A larger aggregate that frequently times out is not an optimization.

A robust approach starts with domain-sized batches. Measure the encoded calldata and response behavior in the target environment. Then adjust the chunk size only when the provider and application workload justify it.

The correct batch is the largest one that remains deterministic and observable under the provider’s actual limits. Larger is not a security property.

Read consistency does not replace freshness

Multicall guarantees same-block reads within the aggregate execution. It does not guarantee that the block is recent by the time the response reaches the browser.

There are two different properties:

  • consistency: all values come from one state snapshot;
  • freshness: that snapshot is recent enough for the application’s decision.

A batch can be perfectly consistent and already stale. The provider may lag behind the network. The browser may display a cached result. The request may complete after several new blocks have been mined.

This distinction becomes critical around transactions. After an approval or deposit, the frontend should not assume that a completed wallet prompt means the indexed or RPC-visible state has updated. It should wait for the transaction receipt according to the application’s confirmation policy, then refetch the relevant multicall. If the provider is behind, the result may still reflect the previous block.

Viem’s multicall is a read primitive. It is not a synchronization protocol for writes.

The same principle applies to simulation. If a transaction is prepared from a multicall result, the state can change before the transaction is mined. The batch gives a coherent input snapshot. It does not reserve that state. Slippage limits, deadline checks, access controls, and contract-level invariants remain necessary.

Do not convert a read snapshot into an assumption that execution will succeed. The transaction is evaluated later.

wagmi integration and the hidden RPC overhead

In React applications, wagmi often supplies hooks around Viem clients and contract reads. The abstraction is useful until it obscures request multiplication.

A component tree can issue several hooks that appear independent:

  • one hook for the balance;
  • one for the allowance;
  • one for token metadata;
  • one for protocol configuration;
  • one for the quote.

Each hook may trigger its own RPC request. React rendering does not turn those hooks into one EVM call. A shared cache can prevent duplicate requests, but it does not provide a same-block snapshot for distinct reads unless the underlying operation is aggregated.

This is the source of much of the wagmi multicall RPC overhead discussion. The overhead is not caused by React itself. It comes from the number of underlying read operations and the absence of a single aggregation boundary.

The correct boundary is usually the domain query. Build one multicall for the values required by the component or action, then expose the parsed result to the component tree. Avoid scattering reads across child components when their values form one logical state.

There are cases where separate hooks remain appropriate:

  • the data has different refresh intervals;
  • one value is optional;
  • one call returns a large payload;
  • the calls belong to different chains;
  • the UI can render each domain independently;
  • a third-party hook owns a specialized cache or invalidation strategy.

Aggregation is not an objective in itself. It is a way to enforce a precise read model.

When Multicall is the wrong data layer

Multicall is well suited to current contract state. It is not a replacement for historical data, event indexing, or full-text querying.

A frontend should not use repeated multicalls to reconstruct a transaction history. That is an indexing problem. The Graph, a custom indexer, or another data service is better suited to queries across blocks and events.

The same boundary applies to large collections. Calling a contract once per token through Multicall may reduce HTTP overhead, but it can still create excessive EVM execution and response data. If the application needs thousands of records, the read primitive is probably wrong.

Use Multicall for:

  • current balances;
  • current allowances;
  • current configuration;
  • bounded portfolio views;
  • related reads needed for one transaction flow;
  • synchronized snapshots across a small or moderate set of contracts.

Use an indexing layer for:

  • historical ownership;
  • event-derived aggregates;
  • transaction history;
  • pagination across large datasets;
  • cross-contract queries spanning many blocks.

Use an oracle integration for externally sourced values. Multicall can read the oracle contract. It does not validate the oracle’s economic assumptions, heartbeat, deviation threshold, or fallback behavior. Reading a Chainlink feed and trusting its answer are separate operations in the security model.

The security autopsy of a failed batch

When a multicall-driven interface behaves incorrectly, inspect the execution path in order.

1. Confirm the chain context.

Verify the chain ID, RPC endpoint, and contract deployment. A correct ABI against the wrong network is still a failure.

2. Confirm the block context.

Determine whether the application requested a specific block or accepted the provider’s latest state. Same-batch consistency does not prove freshness.

3. Inspect the aggregate result.

Check the result count, ordering, and individual status values. Do not collapse partial failure into a successful loading state.

4. Verify ABI and arguments.

Compare the function selector, argument order, tuple structure, and numeric types. Most decoding defects begin here.

5. Review failure policy.

Confirm whether allowFailure matches the business rule. Optional metadata and collateral valuation should not have identical failure semantics.

6. Inspect chunking.

Check batchSize, calldata length, response size, and provider limits. A failure that appears contract-specific may be a transport or payload limit.

7. Check cache invalidation.

After a write, invalidate or refetch the exact reads affected by the state mutation. A successful transaction with stale UI state is an integration defect.

8. Separate read validity from transaction safety.

A coherent snapshot does not guarantee that a later write will succeed. Re-run simulation and enforce contract-side validation where required.

This is the useful discipline of multicall: it reduces the number of moving parts, but it makes the remaining assumptions explicit.

The invariants that should survive deployment

A production implementation does not need a large abstraction. It needs clear guarantees.

The batch should satisfy these conditions:

  • all values used for one decision are read through one coherent aggregate where practical;
  • every required sub-call is checked for success;
  • optional failures are represented explicitly, never replaced with fabricated zero values;
  • the chain and account are part of the cache key;
  • writes trigger targeted refetches after the chosen confirmation point;
  • the batch is split before provider limits are reached;
  • large or historical queries are delegated to an indexing layer;
  • the application never treats same-block data as permanently fresh;
  • transaction parameters are still protected by contract-level invariants;
  • JSON-RPC batching is not presented as equivalent to Multicall3 aggregation.

Viem multicall batching saves RPC limits because it removes unnecessary request boundaries. More importantly, it gives the frontend a deterministic read snapshot. That is the real engineering value.

The implementation should remain narrow. Group related reads. Set failure semantics deliberately. Inspect every result. Respect provider limits. Refetch after state mutations.

Anything less is not batching strategy. It is a new place to hide stale state.

FAQ

What is the difference between JSON-RPC batching and Viem multicall?
JSON-RPC batching groups multiple HTTP requests together but executes them as independent calls that may observe different blocks. Viem multicall uses the Multicall3 contract to execute multiple calls as a single EVM operation against one state snapshot.
Does using multicall guarantee that my data is up to date?
No. Multicall guarantees that all values in a batch are consistent with the same block, but it does not guarantee that the block is recent or that the data is fresh by the time it reaches the user.
How should I handle failed sub-calls in a multicall batch?
You should inspect the status of each individual sub-call rather than assuming the entire batch succeeded. Depending on your business logic, you can set allowFailure to false to revert the whole batch if a required call fails, or handle partial results explicitly.
Can I use multicall to fetch historical data or large datasets?
No. Multicall is designed for current contract state. For historical data, event indexing, or large datasets, you should use an indexing service like The Graph.
Why does my multicall batch fail even if the individual calls work?
Your batch might be exceeding provider limits regarding calldata size, execution cost, or response size. Even with multicall, you must respect provider-specific limits and use appropriate batch chunking.

By Caleb North