Skip to main content
Guides
Category
Operations
Reading time
11 minutes
Published

EVM JSON-RPC batching: when it helps and when it hurts

JSON-RPC batching looks like an easy performance win: put several EVM requests into one HTTP body, send one network round trip, and receive an array of results. For independent reads, that can reduce connection and request overhead substantially. It does not make the underlying RPC work disappear.

The hard parts begin when responses arrive out of order, one entry fails while the rest succeed, a batch crosses a rate or body-size limit, or several calls that should describe one block all use latest. Retrying the whole array can repeat expensive work, and putting dependent calls together does not force the server to execute them sequentially.

This guide explains what a JSON-RPC batch guarantees, how it differs from concurrency and contract multicall, which requests belong together, how to handle partial failures, and how to choose a batch size from measured application results rather than a universal number.

Section 01

What is the short answer?

Batch independent, idempotent reads when network round trips or HTTP overhead are a meaningful part of end-to-end time. Give every request a unique ID, map every response by that ID, pin related reads to an explicit block, and treat each response item as its own success or failure.

Do not use one batch to express dependencies. The JSON-RPC 2.0 specification allows a server to process entries concurrently, in any order, and with any degree of parallelism. It also allows the response array to use a different order from the request array. A call that needs the result of another call belongs in a later request or batch.

Batching is a transport optimization, not a quota bypass, consistency boundary, transaction, retry policy, or guarantee of lower node work. Start with unbatched measurements, add bounded batches, and keep the change only if useful completions improve without unacceptable tail latency, errors, or recovery complexity.

Section 02

What does an EVM JSON-RPC batch actually do?

A normal JSON-RPC request body is one object. A batch request body is a non-empty array of request objects. Each entry still has its own jsonrpc, method, params, and id, and each response still contains either a result or an error.

For example, an application can request a fixed block and one account balance together:

[{"jsonrpc":"2.0","id":"block-19000000","method":"eth_getBlockByNumber","params":["0x121EAC0",false]},{"jsonrpc":"2.0","id":"balance-19000000","method":"eth_getBalance","params":["0x0000000000000000000000000000000000000000","0x121EAC0"]}]

The server can execute those entries sequentially or in parallel. The standard does not promise a single snapshot across them, ordered execution, atomic success, or a shared rollback. The response might list balance-19000000 before block-19000000; the client must look up the matching request by ID.

An empty array is not a valid batch. A request without an ID is a notification, and the server must not return a response for it. Notifications remove the application's ability to confirm success or observe an error, so they are a poor fit for data ingestion and other work that must be auditable.

Section 03

When does batching improve RPC performance?

Batching helps most when requests are numerous, independent, relatively small, and the network round trip is material compared with server execution. Examples include reading several fixed blocks, fetching receipts for transaction hashes you already know, or loading independent balances at one explicit block. Geth's current batch-request guide describes the same best case: larger sets of mostly independent data objects where fewer network delays create a visible speed-up.

The benefit shrinks when one heavy entry dominates the batch. A slow trace, dense eth_getLogs range, or large block response can hold the HTTP response open while quick entries wait. A very large batch can also increase serialization, memory, proxy-body, response-size, and timeout pressure even when the method count is allowed.

Compare batching with ordinary HTTP concurrency rather than with purely sequential calls. Several small requests over reused HTTP connections can already overlap work, isolate failures, and complete incrementally. A batch wins only when its saved overhead exceeds the latency and failure coupling it introduces.

Section 04

Which requests should go in the same batch?

Group calls that share an operational profile: similar cost, deadline, response size, retry policy, and importance. Good candidates are independent current reads for one page load, a bounded group of known transaction receipts, or fixed-block state queries that can be repeated safely.

Separate these classes:

  • Light reads from heavy reads. A trace or wide log scan should not delay balances and block numbers behind the same HTTP response.
  • Live traffic from backfills. A recovery job needs its own concurrency and retry budget so it cannot consume every slot needed at the head.
  • Reads from writes. Transaction submission has an ambiguous outcome after a timeout; it should not inherit a generic read-batch retry policy.
  • Different deadlines. A request useful for 500 milliseconds should not share a batch with work allowed to run for tens of seconds.
  • Different capability requirements. Every entry must be supported by the chosen endpoint and historical range. One accessible head method does not prove the batch's old-state or trace calls are available.

Keep the unit of recovery small. If losing one HTTP response would force the application to repeat too much useful work, the batch is too broad even when it fits the documented limit.

Section 05

Can dependent RPC calls share one batch?

Not when the second call needs data returned by the first. Suppose an indexer must fetch blocks, extract their transaction hashes, and then fetch the receipts. The receipt requests cannot be constructed until the block responses arrive. Use two stages: one bounded batch for blocks, then one or more bounded batches for the resulting receipt hashes. Geth uses this exact dependency pattern in its batch guide.

The same rule applies when a first call resolves the target block, contract address, storage key, or transaction hash used by later calls. A JSON-RPC array is not a workflow language. Array position does not create a happens-before relationship.

If the calls are known in advance but must describe the same chain state, resolve a block number and hash before creating the batch. Pass that explicit block reference to every method that accepts it. Where supported, EIP-1898 block-hash parameters can bind state reads to a particular canonical block instead of repeatedly resolving latest.

Section 06

Does one batch give every call the same block view?

No. JSON-RPC batching defines message transport, not an EVM state snapshot. Because a server may process entries concurrently or in any order, two calls using latest can observe different heads if a block arrives while the batch is running.

For a dashboard, that small difference may be acceptable. For accounting, simulation, indexer validation, or a trading decision, it can produce a combination of individually valid results that is invalid as a whole. Resolve an intentional block first, store its number and hash, and use that block in the batch wherever the method accepts a block parameter.

Some workflows cannot be made consistent by transport batching alone. If several contract reads must execute against exactly one EVM context and the chain supports the required mechanism, a contract multicall or an RPC method designed for aggregated state may be more appropriate. Test its failure semantics and calldata limits separately; it is not the same operation as sending an array of JSON-RPC requests.

Section 07

How should clients handle out-of-order and partial responses?

Create a request table before sending the batch. For each unique ID, store the method, parameters, deadline, retry class, and application destination. When the response arrives:

  1. Reject a response ID that was not in the request table.
  2. Reject a duplicate response ID rather than allowing the later item to overwrite the first silently.
  3. Validate that every non-notification request has exactly one matching response.
  4. Process result and error per entry; do not convert one item error into a fictional failure of successful siblings.
  5. Validate each result's schema and chain or block identity before committing it.
  6. Record unresolved IDs separately from explicit JSON-RPC errors.

The standard requires a response to contain either result or error, never both, and to echo the request ID. It does not require response-array order. Matching by position is therefore a correctness bug even if one endpoint happens to preserve order during a test.

Commit application data according to the workflow's own atomicity rules. A partially successful batch should not advance an indexer cursor past an entry that failed validation or was never returned.

Section 08

How do retries work after a batch failure?

Separate whole-request failures from item failures. If the HTTP request times out, the connection closes, or the response cannot be parsed, the client may not know which entries executed. Idempotent reads can be retried within a total deadline, but use backoff, jitter, and a bounded attempt budget. JSON-RPC IDs correlate responses; they are not server-side idempotency keys.

If the HTTP response is valid and only some entries contain retryable errors, retry only those entries. Replaying the entire batch wastes the successful work and can amplify throttling. Do not retry invalid parameters, unsupported methods, execution reverts, or unavailable historical data as though they were transient network failures.

Transaction submission needs a different path. After an ambiguous eth_sendRawTransaction timeout, persist and query the transaction hash, then rebroadcast the identical signed bytes only when your submission policy calls for it. Never create a replacement transaction merely because a batch response was lost.

Library defaults deserve explicit review. Current ethers JSON-RPC provider options expose batchMaxCount, batchMaxSize, and batchStallTime; disabling batching uses a maximum count of one. Whatever library you use, test whether its retry settings apply to HTTP failures, per-entry JSON-RPC errors, or both.

Section 09

How large should an RPC batch be?

There is no safe universal count. The correct limit is constrained by the endpoint's maximum entries, request-body bytes, response bytes, burst tokens, timeout, method mix, and the amount of work you are willing to repeat after an uncertain failure. Client libraries can add their own count, byte-size, and aggregation-delay ceilings.

Benchmark a grid rather than jumping to the largest accepted array. For each representative method class, compare single requests and several increasing batch sizes under the same total concurrency. Measure:

  • useful results committed per second;
  • p50, p95, and p99 end-to-end latency;
  • time to first usable result and time to the final batch result;
  • request and response bytes;
  • HTTP failures, per-item errors, missing or duplicate IDs, and timeouts;
  • retry amplification and throttling;
  • event-loop, memory, and queue pressure in the application;
  • cost per completed application operation under the provider's current rules.

Stop increasing the batch when useful throughput flattens, tail latency grows beyond the objective, or one failure repeats too much work. Re-run the test for dense log periods, large blocks, old state, and traces instead of transferring a light-read result to every method.

Section 10

Does batching reduce RPC quotas or cost?

Do not assume it does. A provider can count one HTTP request, each JSON-RPC method, weighted method units, returned bytes, or a combination. A batch that saves four HTTP round trips may still consume five billable method units and five rate-limit tokens. Large batches can also fail as a whole when their method count exceeds burst capacity.

Read the current billing and rate-limit contract, then verify it with response headers or usage telemetry. Count retries and partially successful responses. The useful comparison is cost per completed page load, canonical block, or validated trace—not cost per HTTP POST.

With SolidRPC batching, every billable method call inside an authenticated batch consumes one response unit and one rate-limit token. Batching saves round trips, not quota. Keep each batch below the API key's available burst capacity; otherwise the whole request can receive HTTP 429. Keyless public RPC endpoints accept one JSON-RPC call per HTTP request, so batch workloads require an API key.

Section 11

Production JSON-RPC batching checklist

  • Batch only independent work; split dependencies into explicit stages.
  • Give every request a unique, non-null ID and map responses by ID rather than array position.
  • Keep notifications out of workflows that need confirmation or error reporting.
  • Resolve an explicit block before related reads and use its number or hash consistently.
  • Separate light, heavy, live, backfill, read, and write workloads.
  • Define maximum entry count, request bytes, response bytes, aggregation delay, and total deadline.
  • Keep a batch below the endpoint's documented burst and body limits.
  • Validate every result or error independently and detect missing, unknown, or duplicate IDs.
  • Retry only unresolved or explicitly transient read entries when a valid partial response exists.
  • Treat an ambiguous transaction submission as unknown, not failed.
  • Measure useful completions, tail latency, bytes, throttling, retries, and repeat work.
  • Test provider failure, malformed responses, one slow entry, one item error, and a lost HTTP response.
  • Requalify after changing the client library, endpoint, method mix, limits, or chain.

A good batch is not the largest array the endpoint accepts. It is the smallest grouping that removes meaningful transport overhead while preserving clear consistency, failure, and recovery boundaries.

Section 12

Frequently asked questions

Are JSON-RPC batch responses returned in request order?

Not necessarily. JSON-RPC 2.0 allows the server to process and return batch entries in any order. Match every response to its request with the unique id field, never by array position.

Is an EVM JSON-RPC batch atomic?

No. A batch is a transport container for separate calls. Entries can execute independently, one can fail while others succeed, and the batch does not provide a shared EVM snapshot or rollback.

Is JSON-RPC batching the same as Multicall?

No. JSON-RPC batching sends several separate RPC method calls in one protocol message. Contract multicall aggregates contract reads into one EVM call context. They have different consistency, failure, payload, and provider-limit behavior.

Does an RPC batch count as one request for billing and rate limits?

That is provider-specific. Never infer it from the single HTTP POST. SolidRPC counts each billable method inside an authenticated batch as one response unit and one rate-limit token; batching reduces round trips, not quota use.

Workload review

Want us to benchmark your current RPC stack?

Send your providers, routing setup, method mix, or monthly bills. We will compare the complete stack with one SolidRPC integration across cost, useful completion rate, failover, and operational work.