Skip to content
Ethras
Menu
← All articles

Ethras Blog

JSON-RPC Batches: Match Responses by ID and Recover Partial Results

Build a batch reader that handles out-of-order responses, individual errors, missing results, and notifications without losing successful work.

By Ethras Team ·

A dashboard sends two RPC calls together. Both responses arrive, but the current block number appears where the application expected a chain ID. The network worked; the client joined the results incorrectly.

Batching needs a small amount of bookkeeping. Treat each request as a separate operation with its own identity, then let the HTTP request carry those operations together. This article focuses on read requests and the client logic around them.

Know what the envelope promises

JSON-RPC allows a server to process batch entries in any order and return responses in any order. Match them using id. An individual response contains either result or error. A notification omits id and receives no response, including inside a batch. An invalid batch envelope can produce a single error object rather than an array. These are protocol rules, not a promise that every endpoint accepts every request shape. See the JSON-RPC 2.0 specification.

Here is a valid request body containing two independent reads:

[
  {"jsonrpc":"2.0","id":"head-1","method":"eth_blockNumber","params":[]},
  {"jsonrpc":"2.0","id":"chain-1","method":"eth_chainId","params":[]}
]

A possible illustrative response puts the second request first:

[
  {"jsonrpc":"2.0","id":"chain-1","result":"0x1"},
  {"jsonrpc":"2.0","id":"head-1","result":"0x123"}
]

The values above are synthetic examples, not a network observation.

Build the lookup before sending

Our recommendation is to create a request map before serialization. Store the method, application context, deadline, and retry policy under each unique ID. A component should retrieve the answer for head-1, rather than retrieve array element zero.

Prefer short string IDs generated by one request coordinator. Avoid reusing an ID within the same outstanding group. Keep the original type when matching: the string "1" and the number 1 should not silently become the same map key.

On receipt, validate the envelope before updating application state. Check the protocol version, known ID, and presence of exactly one result or error member. An error object needs an integer code and a string message. Check member presence rather than truthiness: null, false, and zero can be meaningful results. Flag duplicate and unexpected IDs instead of allowing the last occurrence to overwrite earlier data.

Keep this validation in one transport module. Repeating it inside every widget makes it easier for one page to accidentally restore positional matching.

Preserve successful siblings

Suppose a batch contains a block read and a method that the endpoint does not expose. Our suggested behavior is to make the block result available while recording the method error against its own ID. Marking the entire collection as failed discards work the application already received.

We recommend three separate diagnostic categories:

  • A transport failure means no usable response envelope was obtained, such as a timeout or an HTML gateway error.
  • An envelope problem means the returned JSON cannot be matched safely to the outstanding requests.
  • An item error means a recognized request returned a JSON-RPC error object.

Give each category a different recovery path. A missing method usually needs a capability decision. A parameter error needs a request fix. A transient read failure may justify a delayed retry. Preserve the original error code and a sanitized message so the caller can decide.

For missing IDs, retain completed entries and mark unresolved requests explicitly. Do not invent an error response on the server's behalf. If notifications were intentionally included, exclude them from the set of expected responses. For application reads that require an answer, our recommendation is to use ordinary requests with IDs.

Respect dependencies and block identity

Batching independent work can reduce network overhead. Geth's batch documentation demonstrates fetching blocks together, then issuing another batch after transaction hashes are available.

We recommend drawing the dependency graph before building the batch. Fetching a block and fetching receipts for its unknown transaction hashes require two stages. Placing speculative dependent work in the first batch does not create an execution order.

Also decide whether related reads must describe one block. For a portfolio snapshot, our suggestion is to select a block first and pass that selection to applicable reads. Repeating latest is a poor application contract for a result that you intend to store as one snapshot.

A block number alone does not identify a particular fork. EIP-1898 defines block-hash selectors and an optional canonicality requirement for specified state methods. Verify support for the exact endpoint and methods before adopting that approach. Record the chosen block identity alongside the assembled result.

Bound recovery work

Our recommendation is to cap batch size, response bytes, and total retry time independently. Tune those caps using your workload. A small group of quick reads and a large historical query need different deadlines even if both fit in one array.

Retry only unresolved, retryable reads. Apply backoff and a finite attempt budget. When splitting a failed group, retain request context so results still reach the correct consumer. If a page is closed or a newer refresh supersedes the old one, cancel or discard the stale work.

Finally, test your parser with shuffled responses, one item error, a missing ID, a duplicate ID, a valid null result, and a single envelope error. Those cases exercise the join logic directly. A batch client is ready when it can explain every outstanding request without confusing a useful result, an individual failure, and an answer that never arrived.

Share this article

Share on X

Review your account and post on X. Opening X or copying text does not send a post.

Explore the RPC docs →