An Ethereum JSON-RPC batch groups requests, but it does not guarantee that every request reads the same block. For a consistent snapshot, resolve the chosen latest, safe or finalized head once, record its number and hash, then use that explicit block identity for every supported state read. Match responses by request ID, check each result, and accept the snapshot only when every required read succeeds in that context.
Two correct balances can create a false total
Imagine a dashboard adding token balances from wallets A and B. In this hypothetical example, the two wallets hold all 160 tokens under consideration. Between consecutive block end states, B transfers 25 tokens to A. There is no minting, burning, transfer fee, rebase or other token activity. Transaction gas is paid in ETH and excluded from this token ledger.
H0 and H1 below are symbolic block hashes, not real network observations. Every balance and block height is invented.
| Read context | Wallet A | Wallet B | Combined tokens |
|---|---|---|---|
| Block 1,000, hash H0 | 100 | 60 | 160 |
| Block 1,001, hash H1 | 125 | 35 | 160 |
| Mixed: A from H1, B from H0 | 125 | 60 | 185 |
| Mixed: A from H0, B from H1 | 100 | 35 | 135 |
Suppose one batch contains two token-balance calls using latest. The B request selects H0 as its execution context and returns 60. The node adopts H1 before the A request selects its context, so A returns 125. Both responses correctly describe their respective selected states. Their response-completion order does not establish that selection order. Adding them produces 185, an apparent gain of 25 tokens that never existed in either complete state.
The apparent increase is 25 ÷ 160 × 100 = 15.625%. Selecting H0 for A and H1 for B instead creates an apparent 25-token loss. Rounding the final total or requesting the data more frequently does not repair this accounting error.
Token balances here mean contract reads such as ERC-20's balanceOf, executed with eth_call. A native ETH balance uses eth_getBalance, which returns wei. The official Ethereum JSON-RPC reference documents those separate methods. An ordinary balanceOf call returns encoded balance data, without a separate RPC result field identifying the selected block hash. Preserve the pinned request context rather than inferring it from the amount or completion time. Confusing ETH with a token quantity would introduce a different error even if every request selected H1.
This is a state-selection problem. The exchange balance snapshot and delta guide addresses a separate reconciliation problem: whether an observation replaces a stored amount or adds a movement. A blockchain snapshot first needs every constituent observation to belong to the intended state.
What the batch actually promises
The JSON-RPC 2.0 batch specification permits concurrent processing, different processing orders and responses returned in any order. Associate each response with its request through id. An array position is not an account identity or an execution timestamp.
Batching remains useful. Geth's batching documentation describes reducing network delays when fetching mostly independent data. That transport benefit does not create a shared Ethereum state context.
Putting eth_blockNumber first in the same array cannot make its result an input to the remaining requests. Those requests already contain their parameters. If each still says latest, the returned height is merely another observation. A client must receive and process a block-selection response before it can construct later requests using that result.
Likewise, sending the batch through one HTTP connection does not establish a snapshot contract. A particular provider might document additional behavior, but ordinary JSON-RPC batching alone supplies no such guarantee. The worked race is a permitted possibility, not a claim that every batch will straddle a block.
Separate freshness, identity and confidence
Choose a state policy before making the reads. The three decisions are related but distinct:
| Decision | Example question | Evidence to retain |
|---|---|---|
| Freshness | How recent must this report be? | Selected block timestamp and observation time |
| Identity | Did every accepted read select one state? | Chain identity and explicit block hash |
| Confidence | What chain stage must the selected block satisfy? | Tag used to select the block and observed result |
In Ethereum's execution API block reference, latest means the client's observed canonical head and can be reorganized. safe and finalized select heads with different consensus confidence. These tags select a moving head; repeating the same tag later can select another block.
Consequently, a finalized report still needs one resolved block identity. Two requests using finalized could observe different finalized heads as the marker advances. They may both satisfy the confidence policy while failing the requirement to describe the same moment.
For a dashboard, stale but coherent values and fresh but mixed values need different labels. A report “as of H0” can remain internally consistent after H1 arrives. Whether its age is acceptable depends on the report's purpose. Store that age instead of silently upgrading the label to current.
This article uses Ethereum state-read semantics. On an L2, establish the chain's own meaning for each supported tag. The sequencer confirmation versus settlement guide explains the additional stage mapping for a standard OP Stack configuration.
Resolve the head, then pin the reads
For a multi-wallet report, use a two-stage collection procedure:
- Verify the intended network and select the required tag. Request its block with
eth_getBlockByNumber, usingfalsefor the full-transaction flag when transaction bodies are unnecessary. Record the returned number, hash and timestamp. - Build every required state request with the selected block identity. Give each request a unique ID tied to its wallet, contract and field. Keep the intended call parameters alongside it.
- Submit those requests together or separately. Accept results only after every required item has a usable response under the selected context.
- Apply the report's canonicality and confidence checks. Publish the values with the selected block evidence and observation time, then start a new collection when freshness requires it.
For supported methods, EIP-1898 defines a block-parameter object containing blockHash. It covers eth_call, eth_getBalance and several other state methods. Its optional requireCanonical field defaults to false; true asks the responding node to reject a block it considers noncanonical. Check method, endpoint and client-library support before relying on this form.
In the example, both balance calls should select H1 explicitly. The intended block parameter has blockHash set to H1's actual 32-byte hash and requireCanonical set according to the report's policy. H1 is shorthand here, not a valid literal RPC parameter. Successful reads return A = 125 and B = 35; their total is 160.
A hash fixes identity. A canonicality check is an observation at query time, not a promise that the block will remain canonical afterward. If the application requires finality, select and validate the appropriate finalized context as well. A shared hash alone does not verify a remote server's answers.
A numeric height is weaker during reorganizations: height 1,001 can identify different blocks at different times. EIP-1898 describes a number-based workaround that compares the canonical hash after each read, but also identifies a race that can evade those checks. When hash selection is unavailable, document that limitation rather than presenting repeated height checks as equivalent assurance.
Keep errors from becoming a plausible portfolio
The collection procedure needs a failure policy because a tidy total can hide missing evidence.
One required response is an error. Retain the successful observations, but do not mark the full snapshot complete. For an informational display, show the missing item explicitly. Never turn an error into a zero balance. JSON-RPC distinguishes a successful result from an error in each response object; a readable batch envelope does not clear the individual items.
A retry reaches a different backend. Retry against the same selected hash where supported. If that backend has not learned the block or cannot serve its state, preserve the failure. Changing only the failed call to latest would combine the original report with a newer state. Either complete the original context or begin a fresh collection for all required items.
The block exists but its state is unavailable. Historical block retrieval and historical state access are different capabilities. Geth's archive documentation explains that historical state access depends on retention and indexing. A header response alone is insufficient evidence that an old balance call can be answered. Choose an endpoint with the required history, or report the snapshot as unavailable.
Calls use incompatible execution inputs. A shared hash does not make different requests interchangeable. Preserve contract address, calldata, caller, value and any deliberate simulation overrides. A caller-sensitive function can return different values for different callers. Geth documents temporary changes through its state and block override objects. An overridden simulation describes its specified scenario; label it separately from an unmodified state observation. The eth_call versus gas estimation guide develops what those simulation results establish.
The total agrees by coincidence. Some quantities can move in offsetting ways, so a plausible sum does not prove coherent selection. Request-context evidence must establish the shared block. Events and trace-derived rows can help explain activity, but they have different coverage; see Ethereum event logs versus internal transactions.
Make the snapshot record reproducible
Keep a small manifest beside the values. Its purpose is to let a second reader distinguish “these calls completed” from “these fields form the accepted report.”
| Manifest field | Record |
|---|---|
| Scope | Network, wallets, contracts, fields and units |
| Selection | Chosen tag, block number, block hash and timestamp |
| Requests | Unique IDs, complete parameters and endpoint |
| Results | Raw return values, decoding rules and item errors |
| Acceptance | Required-item coverage, confidence check and report status |
| Timing | Selection time, completion time and maximum accepted age |
Exercise the collector with deliberate fixtures before trusting its totals. Reorder the responses and verify that A still maps to A. Advance the head between the two latest reads and confirm that the 185-token combination is rejected as a shared snapshot. Then pin H1 and require the 160-token total. Make B's pinned request fail and require an incomplete result rather than 125 or 125 plus a cached 60.
Finally, repeat the same pinned requests after the head advances again. Under unchanged call inputs and a conforming endpoint that can serve H1, the report should still describe H1; advancing the head is not permission to change its context. These are validation fixtures, not results from a live provider test.
Before accepting a multi-call balance total, attach one explicit block identity to every required read and preserve every item outcome. If a field cannot meet that contract, keep the report incomplete until the shared state can be established.
