eth_getLogs block range too large: the block range limit per chain and how to split queries
With an API key, one eth_getLogs request may span 1,000 blocks on HyperEVM; each chain publishes its own max_logs_block_range. On logs_range_too_large, split the inclusive block interval within that limit and advance only after the chunk succeeds.
Direct answer
With an API key, a single eth_getLogs request is limited to the target chain's max_logs_block_range from GET /v1/chains: 1,000 blocks on HyperEVM and 1,000 on Robinhood Chain, counting toBlock − fromBlock + 1 blocks; the table for every chain follows. Checked . Exceeding it returns HTTP 200, JSON-RPC -32602 and error.data.reason: logs_range_too_large, with retryable: false (see the error catalog). Split the interval into [from, min(from + max − 1, end)] and advance to the previous chunk's end plus one after success.
- First step: Run
curl -s "https://api.blockvectra.com/v1/chains". With a key, readmax_logs_block_range,methods.allowandmethods.deny; without a key, first checkpublic.methods, thenpublic.logs,public.history_blocksand the general public policy. - Done when:
logs-minimal.mjsoutputs each completed chunk'sfromBlock,toBlockandresultarray through your chosenTO_BLOCK, without an HTTP or JSON-RPC error.
Chain parameters and access options.
- Programmatic sign-up: Autonomous AI agents, automated scripts, and CI pipelines can sign in and provision API keys using Ethereum wallet signatures (EIP-191) without a browser. Follow the Programmatic sign-up guide.
- Web console: Sign up via Ethereum wallet signature, and generate an API key in the Console. See the Quickstart guide for setup details.
Access mode
With an API key, read the top-level max_logs_block_range. For anonymous access, first confirm that public.methods includes eth_getLogs, then obey public.logs together with public.history_blocks and the general public rate/batch policy. public.logs.max_block_range is a per-query range; public.history_blocks is the block-reference lookback window, separate from the state window. Anonymous blockHash support is determined by public.logs.block_hash_supported. Missing or unknown policy does not mean unrestricted access. Mainnet permissions do not imply testnet permissions. The example below uses an API key.
Save this as logs-minimal.mjs, set BLOCKVECTRA_API_KEY, the contract address LOG_ADDRESS, and a confirmed block window in FROM_BLOCK and TO_BLOCK, then run node logs-minimal.mjs with Node.js 24 or later. Select a chain with CHAIN; the default is robinhood_mainnet.
const { BLOCKVECTRA_API_KEY: key, LOG_ADDRESS: address, FROM_BLOCK, TO_BLOCK } = process.env;
if (!key || !/^0x[0-9a-f]{40}$/i.test(address ?? '')) throw new Error('Set BLOCKVECTRA_API_KEY and LOG_ADDRESS');
if (![FROM_BLOCK, TO_BLOCK].every(value => /^(0x[0-9a-f]+|[0-9]+)$/i.test(value ?? ''))) {
throw new Error('Set FROM_BLOCK and TO_BLOCK to nonnegative block numbers');
}
const start = BigInt(FROM_BLOCK), end = BigInt(TO_BLOCK);
if (start > end) throw new Error('FROM_BLOCK must not exceed TO_BLOCK');
const chainSlug = process.env.CHAIN ?? 'robinhood_mainnet';
const chainsUrl = 'https://api.blockvectra.com/v1/chains';
const catalogResponse = await fetch(chainsUrl, { signal: AbortSignal.timeout(15_000) });
if (!catalogResponse.ok) throw new Error(`Chains HTTP ${catalogResponse.status}`);
const catalog = await catalogResponse.json();
const chain = catalog.chains.find(item => item.chain === chainSlug);
if (!chain || !Number.isSafeInteger(chain.max_logs_block_range) || chain.max_logs_block_range <= 0) {
throw new Error('Missing or invalid max_logs_block_range');
}
const matches = pattern => pattern.endsWith('*') ? 'eth_getLogs'.startsWith(pattern.slice(0, -1)) : pattern === 'eth_getLogs';
if (!chain.methods?.allow?.some(matches) || chain.methods?.deny?.some(matches)) {
throw new Error('eth_getLogs is unavailable on this chain');
}
const max = BigInt(chain.max_logs_block_range);
const rpcUrl = new URL(`./${chainSlug}`, chainsUrl).href;
const hex = value => `0x${value.toString(16)}`;
for (let from = start; from <= end;) {
const to = from + max - 1n < end ? from + max - 1n : end;
const response = await fetch(rpcUrl, {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
headers: { 'Content-Type': 'application/json', 'x-api-key': key },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs',
params: [{ address, fromBlock: hex(from), toBlock: hex(to) }] }),
});
const body = await response.json();
const valid = body !== null && typeof body === 'object' && !Array.isArray(body) &&
body.jsonrpc === '2.0' && body.id === 1 && !('error' in body) && Array.isArray(body.result);
if (!response.ok || !valid) {
throw new Error(`RPC HTTP ${response.status}: ${JSON.stringify(body?.error ?? 'Invalid result')}`);
}
console.log(JSON.stringify({ fromBlock: hex(from), toBlock: hex(to), result: body.result }));
from = to + 1n;
}Each output line is a completed chunk. Any HTTP or JSON-RPC error stops the example without skipping the failed chunk. See the batch and rate-limit guidance below for handling 429.
Empty logs near the chain head
An empty result: [] can be valid when no logs match the filter. If receipts appear to contradict it, compare the same block hash on the same endpoint and apply the same address and topics filters to the receipt logs; receipts can contain unrelated logs.
A Nethermind 2.1.0 issue reports a different case: eth_getLogs with blockHash temporarily returned [] before the block became head, while receipts for that hash already contained logs. This is a report about that client and version, not a verified BlockVectra incident. Reducing the block range does not diagnose that discrepancy.
For a comparable discrepancy, save the block hash and height, exact filter, observed head, and responses with timestamps; remove API keys and credentials. If the endpoint supports receipt queries, compare only logs that match your filter. Keep that block unresolved in your indexer while checking the contradiction, using bounded retries and your reorg/checkpoint recovery policy. Do not treat every empty result as a failure. The minimal example above queries a confirmed block window; it is not a near-head completeness check.
BSC: a saved non-empty event comparison
BNB Chain's public endpoint documentation says its listed public mainnet endpoints disable eth_getLogs. A method being unavailable is different from a successful result: []; that policy alone does not explain a successful empty array when matching receipt logs exist. For BlockVectra, BSC access options require an API key for eth_getLogs; the public RPC is not an entry point for this method.
The fixed historical sample records three requests on 2026-10-25 at 13:26:17–21 UTC: chain ID 56, a fixed transaction receipt, and an address-only, single-block logs query at block 129476271 (0x7b7a6af). The query returned four logs. One ERC-721 Transfer for token 1811 matched the receipt on eight fields: address, topics, data, blockHash, blockNumber, transactionHash, transactionIndex, and logIndex.
Download the ZIP for that fixed version, extract it, and enter the directory containing the sample files. Downloading needs a network connection; the checker itself runs offline with Node.js 22 or later, without an API key or npm install:
node verify-sample.mjs observed-sample.jsonExpected output: PASS 1 receipt event matched among 4 returned logs. This checks one event in a historical archive, not current historical availability, whole-block or whole-history completeness, canonical status, performance, or the cause of an incident. This sample is not input for the whole-block checker below and is not a whole-block acceptance check.
eth_getLogs limits by chain
These are each chain's max_logs_block_range values published by GET /v1/chains. "Not published" does not mean unlimited. Also check methods.allow and methods.deny before calling, with deny taking precedence; a block span limit is separate from result count or query duration limits.
| Chain | Chain slug | max_logs_block_range (blocks) |
|---|---|---|
| Arbitrum One | arb_mainnet | 1,000 |
| Arc | arc_mainnet | 1,000 |
| Avalanche C-Chain | avax_mainnet | 1,000 |
| Base | base_mainnet | 1,000 |
| Berachain | berachain_mainnet | 1,000 |
| Boba | boba_mainnet | 1,000 |
| BNB Smart Chain | bsc_mainnet | 1,000 |
| Celo | celo_mainnet | 1,000 |
| Ethereum | eth_mainnet | 1,000 |
| Ethereum Sepolia | eth_sepolia | 1,000 |
| Fraxtal | fraxtal_mainnet | 1,000 |
| Gnosis | gnosis_mainnet | 1,000 |
| HyperEVM | hyperevm_mainnet | 1,000 |
| Ink | ink_mainnet | 1,000 |
| Katana | katana_mainnet | 1,000 |
| Linea | linea_mainnet | 1,000 |
| Metal L2 | metall2_mainnet | 1,000 |
| Mode | mode_mainnet | 1,000 |
| OP Mainnet | op_mainnet | 1,000 |
| Polygon | polygon_mainnet | 1,000 |
| Robinhood Chain | robinhood_mainnet | 1,000 |
| Robinhood Chain Testnet | robinhood_testnet | 1,000 |
| Rootstock | rootstock_mainnet | 1,000 |
| Soneium | soneium_mainnet | 1,000 |
| Sonic | sonic_mainnet | 1,000 |
| Unichain | unichain_mainnet | 1,000 |
| World Chain | world_mainnet | 1,000 |
| X Layer | xlayer_mainnet | 1,000 |
| Zora | zora_mainnet | 1,000 |
eth_getLogs block range limits
When calling the JSON-RPC eth_getLogs method, the block span of a single request is calculated as toBlock − fromBlock + 1 and cannot exceed the target chain's published max_logs_block_range.
This limit varies by chain. Per-chain parameters are published through the public endpoint GET /v1/chains (chains are listed on Supported Chains). This endpoint is unauthenticated and unbilled. When developing client applications, query this endpoint dynamically at runtime rather than hardcoding block range limits into your code.
Filter fields fromBlock and toBlock default to latest when omitted or null.
Common error messages, verbatim
Distinguish block span, result count, and query duration: the same JSON-RPC code can describe different failures.
| Error text / identifier | Source | What to do |
|---|---|---|
eth_getLogs block range too large: max <N> blocks; -32602; logs_range_too_large | BlockVectra error catalog | <N> is the chain's max_logs_block_range; reduce the span before resending. Retrying unchanged does not help. |
query block range exceeds server limit, narrow your filter: <N> | Erigon eth_getLogs source | <N> is that node's range limit; reduce the queried interval before resending. |
query returns too many logs, narrow your filter: <N> | Erigon eth_getLogs source | <N> is that node's result limit; reduce the interval and narrow address and topics. A single block may still need more specific filters. |
In these message templates, <N> is replaced by the endpoint's limit. Third-party messages refer to their own endpoints and limits; wording can vary by client version. For BlockVectra, use /v1/chains and error.data.reason.
Read the JSON-RPC error behind a Web3.py HTTPError
With the synchronous HTTP provider, Web3.py posts through Requests and calls raise_for_status() on the default, non-streaming path. A 4xx or 5xx reply therefore surfaces as requests.HTTPError (as in web3.py#3915), and any JSON-RPC error the server sent is in exc.response. AsyncHTTPProvider and stream=True are separate paths; this section does not cover them.
A Requests Response is falsey for 4xx/5xx, so test exc.response is None, never if response. w3 and filter_params below are your existing client and filter; wrap the call you already make:
import requests
def rpc_error_from_http(exc):
response = exc.response
if response is None:
return None
try:
body = response.json()
except ValueError:
return None
error = body.get("error") if isinstance(body, dict) else None
if not isinstance(error, dict):
return None
data = error.get("data")
reason = data.get("reason") if isinstance(data, dict) else None
code = error.get("code")
return {
"status": response.status_code,
"code": code if isinstance(code, int) and not isinstance(code, bool) else None,
"reason": reason if isinstance(reason, str) else None,
}
try:
logs = w3.eth.get_logs(filter_params)
except requests.HTTPError as exc:
info = rpc_error_from_http(exc)
if info is not None:
print(info)
raiseThe helper only reads the status, code and data.reason; it prints no headers, key, request or full body, and returns None when the body is not a JSON object with an error object (for example a 502 HTML page). The raise keeps the original HTTPError, and a 200 reply never reaches the except. The diagnosis is not a valid RPC response and carries no logs.
How to read it:
- A BlockVectra span overrun is not an HTTPError: it returns HTTP 200 with
error.data.reason: logs_range_too_large, as described below. -32602alone does not mean the span is too large. On another provider, check the actualmessage,codeanddatabefore changing the range.- On 429 or 502, do not shrink the range, retry without a bound, or advance your checkpoint. Retry a bounded number of times, honoring
Retry-Afterwhen present, and move forward only after a chunk succeeds.
Exceeding the block span limit
When a single request's block span toBlock − fromBlock + 1 exceeds the chain's max_logs_block_range, the request is rejected with HTTP 200 and a JSON-RPC error:
- Error code:
-32602 - Error message:
eth_getLogs block range too large: max <N> blocks - Billing status: Not billed.
Example request
This request exceeds the limit only if its block span is larger than the target chain's current max_logs_block_range:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [
{
"fromBlock": "0x45a2409",
"toBlock": "0x45a27f1"
}
]
}Example response
The corresponding error response example:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "eth_getLogs block range too large: max <N> blocks"
}
}Where <N> is the target chain's max_logs_block_range (published via GET /v1/chains).
In a batch request containing multiple calls, if an eth_getLogs call exceeds the block span limit, that specific item returns the -32602 error above and is not billed.
Performing chunked queries
To query logs over a large block interval, first query the target chain's max_logs_block_range, divide the target interval into contiguous chunks of [from, from + max - 1], and send sequential requests while aggregating the results.
The following examples use robinhood_mainnet to demonstrate chunked queries:
export BLOCKVECTRA_API_KEY="rgw_your_api_key"
# 1. Read max_logs_block_range from the public chains endpoint (unauthenticated, unbilled)
curl -s "https://api.blockvectra.com/v1/chains"
# 2. Make a single compliant request within the chain's max_logs_block_range (toBlock - fromBlock + 1)
curl -s "https://api.blockvectra.com/v1/robinhood_mainnet" \
-H 'Content-Type: application/json' \
-H "x-api-key: $BLOCKVECTRA_API_KEY" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [{
"address": "0x1111111111111111111111111111111111111111",
"fromBlock": "0x45a2409",
"toBlock": "0x45a246c"
}]
}'Test response handling offline
Use the 35 synthetic response cases with a mock transport to check chunk boundaries, HTTP/JSON-RPC failures and successful empty results. See the format and expected outputs and MIT license.
For checking progress commits, timestamp-start estimation and stale writes after cancellation, download the v0.1.0 synthetic evaluation fixtures: evm-log-backfill-checkpoints v0.1.0. The 11 cases are teaching and evaluation data, not real chain data; they cover checkpoint, timestamp-start and writer-fencing JSONL groups. The release also includes a tar.gz archive, fixtures.manifest.json and SHA256SUMS. These fixtures are separate from the 35 response cases above and from the resumable client. They need no install, key or network and run on Node.js 22. The commit-fence cases model synchronous in-memory state only, not real database atomicity or persistence.
Resumable measurement client (download)
Download getlogs-resumable.mjs and run it with Node.js 24 or later. It has no dependencies, calls only the fixed production catalog and RPC endpoints, validates eth_chainId against the catalog's chain_id, and splits the window by the live max_logs_block_range after checking methods.allow and methods.deny.
export BLOCKVECTRA_API_KEY="rgw_your_api_key"
export LOG_ADDRESS="0x1111111111111111111111111111111111111111"
export FROM_BLOCK="0x45a2409"
export TO_BLOCK="0x45a2900"
export CHAIN="robinhood_mainnet"
export CHECKPOINT_FILE="./getlogs-checkpoint.json"
node getlogs-resumable.mjsOptional: set CHUNK_BLOCKS (a positive decimal integer, for example CHUNK_BLOCKS=100) to request fewer blocks per call than the catalog limit. The span used is min(CHUNK_BLOCKS, max_logs_block_range); a larger value is clamped to the catalog limit, not rejected, and an invalid value stops before any request. A smaller span means more requests and may cost more; it is not a guarantee about result size.
It is an execution setting, not part of the task identity. For the same chain, address, from and to, you may change only CHUNK_BLOCKS and continue with the same checkpoint file; it only applies to blocks not yet saved, and saved chunks are never re-requested. total_chunks is the saved chunks plus the chunks needed for the remaining blocks at the effective span, and the 20-chunk budget counts saved chunks, so lowering CHUNK_BLOCKS on resume can stop the run before the first eth_getLogs.
Three limits are separate: the block span (max_logs_block_range), the number of logs a request may return, and the 2 MiB response body cap. If a chunk fails on the result-count or size limit, the client does not commit it, advance, shrink the range or retry on its own. To change the window, address or chain, use a new checkpoint file and keep the original file; this client does not overwrite or migrate a task automatically. This is not evidence that any real run has been resumed, nor a claim of no missing data or finality.
One process may use a checkpoint file at a time. The client takes a lock file at <CHECKPOINT_FILE>.lock, writes each candidate checkpoint as one JSON document through a 0600 temporary file, fsync and atomic rename, and stops on the first error without skipping a chunk or retrying. If the lock remains after a crash, verify that no process owns it and remove it manually. A checkpoint is rejected if its task binding, chunk order, continuity or nextBlock does not match; it is never silently restarted, and an existing file that is not a valid checkpoint object is rejected before any request instead of being treated as absent. The budget is 20 saved chunks for the whole task, including resumed chunks, so the client stops before requesting a chunk that would exceed it. Saved chunks stay valid when the catalog's max_logs_block_range changes: only the remaining window is split by the current value. Each response and the checkpoint file are read through a byte-bounded path capped at 2 MiB; an oversized response is canceled, and the last committed checkpoint is kept.
status: "complete" only means every block in the fixed requested interval was read successfully. It does not claim upstream completeness or finality, and it does not make retries exactly-once: choose a confirmed window, and validate reorgs independently with block hashes or overlapping re-queries. This is a client-side measurement example: a default run does not add the internal QA suffix. Only internal automated tests that explicitly set BLOCKVECTRA_INTERNAL_QA=1 for the CLI, or pass internalQa: true when calling the library, add the BlockVectraQA/1 User-Agent suffix; internal automated tests must set it. Its output reports client-observed attempt latency, separates catalog, chain ID and eth_getLogs request/success counts, counts new_attempts for eth_getLogs only, and reports committed chunks separately from successful eth_getLogs RPCs. request_observations has one ordered row for every actual fetch (catalog, chain ID and eth_getLogs), so its count denominator is all requests, unlike new_attempts; task or checkpoint failures before a fetch add no row. Each row records the stage, the observed HTTP status or null when unknown, application body bytes actually consumed (0 if unread, partial if reading stops early, never Content-Length or wire size), body_read_complete (true only after the reader signals done), and an outcome: json_received, http_error, transport_error, timeout, invalid_json, response_limit, unreadable_body or invalid_response. json_rpc_error_present is null when unknown, for catalog rows, and for invalid JSON or nonobject RPC bodies; otherwise it only reports whether a parsed RPC object has its own error field and does not validate that field. json_received means valid parsed JSON, not semantic RPC acceptance: only stage_successes and ok attempts count accepted stages and RPCs. HTTP error bodies stay unread. committed_chunks is the number of chunks committed successfully in this run; the total saved ranges equal resumed_chunks + committed_chunks. Offline mock runs are not production measurements, and the example publishes no p95, p99 or SLA.
Reconcile one block offline
Download getlogs-reconcile-block.mjs to check an eth_getLogs result against the block's receipts, so a transaction that a partial result left out does not go unnoticed. It needs Node.js 24 or later, has no dependencies and makes no network calls.
node getlogs-reconcile-block.mjs block-material.jsonThe file (at most 2 MiB) holds one block: { "block": { "hash", "number", "transactions": [txHash] }, "receipts": [full receipt RPC results, one per transaction, any order], "logs": [eth_getLogs result], "filter": { "address", "topics" } }. transactions is the full list of transaction hashes, not transaction objects. Other standard fields such as parentHash, status or logsBloom are ignored. Across the receipts in block order, logIndex must run 0, 1, 2, and so on without gaps or repeats, even for logs the filter excludes. filter is optional: address is one address or an array, and each topics position is null, one topic or an array of topics. Filter semantics follow go-ethereum's filterLogs (an empty address list or empty topic list matches anything, and a topics position the log does not have never matches); other providers may differ. The script applies it to every receipt log, then compares the result with logs by block hash, transaction hash and log index, including address, topics, data, block number and transaction index.
It prints { status, reason, counts } and exits 0 for consistent, 1 for inconsistent (a log is missing, extra or changed) and 2 for unresolved (missing or malformed fields, a block-wide logIndex that is not continuous, receipts that do not cover the block's transactions, logs from another block, duplicate identities, an invalid filter). Stop on 1 or 2 and keep the range unresolved. The script never reads or writes a checkpoint. consistent only means the supplied material agrees with itself; it does not show that the block is canonical, check a receipts root or prove that history is complete.
Batch request considerations
If you consider packing multiple chunked queries into a single JSON-RPC batch request, keep the batch and burst capacity rules in mind:
- Batch size limit: Batch requests accept 1 to 100 calls. Submitting more than 100 calls is rejected with HTTP 200 and error code
-32600 batch too large: max 100 calls(not billed). - Single-request burst capacity: If the summed CU weight of the calls in one request exceeds the key's burst capacity (
burst_cu), the request is rejected with HTTP 429-32022 request cost <N> CU exceeds burst capacity <M> CU(not billed); split it into smaller batches. - Insufficient bucket capacity: If the sum of full weights does not exceed burst capacity but the token bucket lacks enough available capacity, the service returns HTTP 429 with error code
-32005 rate limit exceededandRetry-After; see What is not billed: error codes and billing rules for retry and billing details.
Therefore, when performing large-scale log queries, sequential chunked queries are recommended; if batching, keep the number of calls per batch small enough so the sum of full weights remains within the burst capacity.
Related guides and billing rules
- See the eth_getLogs method reference for filter parameters, return values, and CU weights.
- See the logs_range_too_large error reference for error details and recommended actions.
- For a comparison between
eth_getLogsand the Data API transfers endpoints (address transfers and token transfers), including coverage and finality differences, see Recent node data vs indexed history: when to use eth_getLogs and when to use the transfers API. - For full details on compute units (CU), hourly settlement, and non-billed error responses, see What is not billed: error codes and billing rules.
Next steps
- Browse the datasets directory to see every dataset BlockVectra indexes.
- See the free plan and pricing to check what your account includes.
- Log in to the console to create an API key.
Last updated:
Logs vs Transfers API
Choose eth_getLogs for contract event logs or the Token Transfers API for indexed ERC-20 transfer history. Compare block ranges, pagination, coverage and finality.
Reading CU pricing
At current published rates, eth_call costs 15 CU per call, or $1.50 per million calls before available free credits; fetch the current plans API for your workload estimate.