eth_getLogs Block Range Limits by Chain, Measured

No standard eth_getLogs block range limit exists. We measured 15 chains on 24 October 2026, from 10 blocks to no cap, and what each -32005 error means.

eth_getLogs Block Range Limits by Chain, Measured

There is no standard eth_getLogs block range limit. On 24 October 2026 we measured public endpoints on 15 chains, and the widest block window they accepted on a filter that matches nothing ran from 10 blocks to the whole chain. On many endpoints a result cap, anywhere from 5,000 to 200,000 logs, bites before the block range does. The error code will not tell you which limit you hit: -32005 meant “too many results” on some providers and “this method is off” on the BNB Chain dataseeds. Read the message, not the code.

This page is the reference: what each endpoint allowed, what each error string means and what to do about it. For the indexing loop that sits on top of it (checkpoints, reorgs, storage), the next step is the production eth_getLogs pipeline.


How we measured

Every public endpoint number below comes from calls we made on 24 October 2026 between 12:06 and 12:59 UTC, from one home connection, with Node 24.19.0 fetch and a 30 second client timeout. We repeated the whole set later that day, between 13:17 and 14:00 UTC. The mechanism, code and message of every limit came out the same, but block counts on busy filters move with traffic, so where the two runs differ we give the newer figure or both. Public endpoints are shared and rate limited, so your numbers can differ. A few endpoints (dRPC, publicnode, the Optimism public endpoint and 48.club) gave different answers between calls, and we mark where that matters. The public endpoint measurements did not touch BoltRPC. The one BoltRPC measurement has its own section near the end.

Two terms, because providers use them differently:

  • Window: the number of blocks in [fromBlock, toBlock], both ends included.
  • Range: toBlock - fromBlock, which is the window minus one.

We report the window that passed and the next one that failed, and the range where the error message states it. The newest block of every window was the chain head minus 5.

Each endpoint got three filters, with USDT where the chain has it and USDC otherwise:

  • Busy with address: the token contract plus the Transfer topic. In our runs that was roughly 65 to 110 logs per block on Ethereum, Base and BNB Smart Chain, about 5 on Polygon and about one every two blocks on Arbitrum.
  • Busy without address: the Transfer topic alone, so every ERC-20 on the chain.
  • Narrow: the token contract, the Transfer topic and a random sender address as the second topic. It always returns zero logs, which isolates the block range rule from the result count rule.

For each filter we climbed a ladder of window sizes (1, 10, 100, 500, 1,000, up to 1,000,000 blocks) and then bisected between the last pass and the first failure. A separate depth probe used a 10-block window ending N blocks behind the head, to tell “window too wide” from “start too far back”.

One trap is worth passing on. Our first full run used a mistyped Transfer topic hash, so every busy filter matched zero logs and looked like it had no limits. We threw that run away. If a busy test returns zero logs, you tested the narrow case.

The spread, from smallest to largest

The widest window that passed on the narrow filter, by endpoint family:

  • 10 blocks: the public blastapi.io hosts for Ethereum, Base and Arbitrum One.
  • 100 blocks: the Avalanche C-Chain public endpoint.
  • 101 blocks: the dRPC free tier on Ethereum, Polygon, Base, Arbitrum, Optimism, zkSync Era, Avalanche C-Chain and Monad. The official Monad endpoint matches at 101.
  • 1,000 blocks: rpc3.monad.xyz.
  • 10,000 blocks (windows of 10,000 or 10,001): the Linea, Plasma, Unichain and Mantle official endpoints, publicnode on Polygon and rpc-bnb.blockmachine.io.
  • 50,001 blocks: publicnode on Base, Avalanche C-Chain, Scroll, Sonic and Mantle.
  • About 10,000,000 blocks: the Arbitrum One public endpoint when the filter has an address. Without an address it is 30,000.
  • The whole chain: Scroll, zkSync Era, Sonic and MEV Blocker (Ethereum), tried from block 0 on a filter that matches nothing.

That last line comes with a warning. A narrow filter returns zero logs, so “no cap” says nothing about a busy contract. For those four endpoints, the result cap is what limits you.

Measured limits by chain

Each entry gives the window that passed and failed on the narrow filter, the limit that bites first on a busy filter, and the error as HTTP status, code, message. Full message strings are in the catalogue further down. BNB Smart Chain has its own section below.

  • Ethereum (chain 1)
    • ethereum-rpc.publicnode.com: no width cap on the narrow filter, but it refuses a start more than about 9,843 to 10,000 blocks behind the head (403, -32602, “Archive requests require a personal token”). Busy with address: 2,000-block windows (about 209,000 to 216,000 logs) passed in every run, and in the first run 2,562 blocks hit our 30 second timeout. Without an address only one block works (200, -32701, “Please specify an address”).
    • eth.drpc.org: window 101 passed, 102 failed (400, code 35, and the message says 10,000). Busy without address: 32 blocks passed (19,769 logs), 33 failed with -32602 “query exceeds max results 20000”. The cap is not strictly on returned logs: one 44-block window returned 29,589 logs and passed, and the same window size failed three minutes later.
    • rpc.mevblocker.io: the whole chain on the narrow filter (a 26,262,495-block window, zero logs, 305 ms). The result cap bites first: with an address, window 112 passed and 115 failed (200, -32005, “query returned more than 10000 results”).
  • Polygon (chain 137)
    • polygon-bor-rpc.publicnode.com: window 10,001 passed, 10,002 failed (200, -32701, “exceed maximum block range: 10000”). With an address, 6,562 blocks passed (32,933 logs in 29.8 s) and 6,718 hit our timeout. Without an address, one block.
    • polygon.drpc.org: window 101 passed, 102 failed, same message as Ethereum.
  • Base (chain 8453)
    • mainnet.base.org: eth_getLogs always refused, see the oddities section.
    • base-rpc.publicnode.com: window 50,001 passed, 50,002 failed (200, -32701, “exceed maximum block range: 50000”). The result cap of 20,000 bites first on USDC: 281 blocks passed (19,939 logs), 287 failed (-32602, “query exceeds max results 20000”).
  • Arbitrum One (chain 42161)
    • arb1.arbitrum.io/rpc: with an address, about 10,000,000 blocks (the message states 10,000,000). Without an address, windows up to 30,000 passed (a range of 29,999) and 30,001 failed (200, -32602, “query spans 30001 blocks … but only 30000 are allowed for this request”). The log cap bites first: with the USDT address, 18,898 blocks passed (exactly 10,000 logs) and 18,899 failed (200, -32000, “logs matched by query exceeds limit of 10000”). The first run passed 23,252 blocks before the same failure, because the number of logs per block moved with traffic.
    • arbitrum-one-rpc.publicnode.com: tiny. It has a start-depth limit of roughly 50 to 100 blocks behind the head (403, -32602, archive requests need a token). The cut-off moved between runs. The re-run passed a narrow window of 88 blocks and failed 89, and 10-block windows ending up to 80 blocks behind the head passed while one ending 100 behind failed.
  • Optimism (chain 10)
    • mainnet.optimism.io: nondeterministic, see the oddities section.
    • optimism-rpc.publicnode.com: start-depth rule only (9,843 passed, 10,000 failed). A 9,843-block window with an address passed with 16,061 logs.
  • Avalanche C-Chain (chain 43114)
    • api.avax.network/ext/bc/C/rpc: window 100 passed, 101 failed, on busy filters too (25 logs with an address, 185 without). 200, -32602, “block range too large”.
    • avalanche-c-chain-rpc.publicnode.com: window 50,001 passed, 50,002 failed. With an address a 50,001-block window passed with 13,243 logs in 6.6 s.
  • Linea (chain 59144)
    • rpc.linea.build: window 10,001 passed, 10,002 failed (200, -32602, “range 10001 exceeds limit of 10000”). The 10,000-result cap bites first without an address: 6,718 blocks passed (9,994 logs), 6,875 failed (200, -32005, “query returned more than 10000 results”).
    • publicnode: start-depth limited to about 100 blocks. Narrow window 95 passed, 96 failed in the re-run (92 and 94 in the first run).
  • Scroll (chain 534352)
    • rpc.scroll.io: the whole chain on the narrow filter (35,481,759 blocks, zero logs, 129 ms). The 200,000-result cap bites: without an address 87,500 blocks passed (199,824 logs) and 89,062 failed. With an address 243,750 passed (153,308 logs in 17.6 s) and 250,000 hit our timeout. 200, -32200.
    • publicnode: window 50,001 passed, 50,002 failed.
  • zkSync Era (chain 324)
    • mainnet.era.zksync.io: the whole chain on the narrow filter (72,619,487 blocks, zero logs). The 10,000-result cap bites: without an address 2,000 blocks passed (9,945 logs) and 2,046 failed. With an address 140,625 passed (9,695 logs) and 143,750 failed in the first run; in the re-run both passed, with fewer logs (5,963 and 6,114), so that boundary depends on volume. 200, -32602.
  • Sonic (chain 146)
    • rpc.soniclabs.com: the whole chain on the narrow filter (81,554,896 blocks, zero logs). A 5,000-result cap: with an address 10,937 blocks passed (4,868 logs) and 11,250 failed. Without an address 2,703 passed and 2,750 failed. 200, -32000, “the limit is 5000”.
    • publicnode: window 50,001 passed, 50,002 failed, plus the same 5,000-result cap.
  • Monad (chain 143)
    • rpc.monad.xyz: window 101 passed, 102 failed (413, -32614, “eth_getLogs is limited to a 100 range”).
    • rpc3.monad.xyz: window 1,000 passed, 1,001 failed (200, -32062, “Block range is too large”).
  • Plasma (chain 9745)
    • rpc.plasma.to: window 10,001 passed, 10,002 failed (413, -32614, “eth_getLogs is limited to a 10,000 range”). A 10,001-block window passed with 13,878 USDT0 logs in the first run. The re-run of the same window failed with 200, -32602, “query exceeds max results 20000”, so Plasma also has a 20,000-result cap.
  • Unichain (chain 130)
    • mainnet.unichain.org: window 10,001 passed, 10,002 failed (400, -32602, “block range greater than 10000 max”). A 10,001-block window passed with 2,076 logs with an address and 4,857 without. publicnode: start depth 9,843 passed, 10,000 failed.
  • Mantle (chain 5000)
    • rpc.mantle.xyz: window 10,001 passed, 10,002 failed (400, -32602, “block range greater than 10000 max”). A 10,001-block window passed with 211 logs with an address and 6,257 without.
    • mantle-rpc.publicnode.com: window 50,001 passed, 50,002 failed. With an address 50,001 blocks passed with 644 logs.

The same block count is a different amount of time

A fixed block cap means very different time spans. We measured block times from eth_getBlockByNumber timestamps at 12:47 UTC. A 10,000-block window is about:

  • 33.5 hours on Ethereum (12.060 s per block)
  • 5.6 hours on Base (2.000 s)
  • 4.2 hours on Polygon (1.500 s)
  • 1.25 hours on BNB Smart Chain (0.450 s)
  • 50 minutes on Monad (0.302 s)
  • 46 minutes on Arbitrum One (0.274 s)

If you size a backfill in days, do the division per chain. A one-day backfill on BNB Smart Chain is 192,000 blocks at the measured rate.

Error strings and what to do about them

These are the strings we saw, grouped by what they mean. The group matters more than the code, because the codes overlap.

Result count: the window is fine, the answer is too big

  • -32005: “query returned more than 10000 results. Try with this block range [0x1EE7816, 0x1EE908A].” (Linea, MEV Blocker). Infura documents the same code and the first half of that wording (“query returned more than 10000 results”), without the block range hint. Its page also lists a 10-second query limit, with the error “query timeout exceeded”.
  • -32602: “Query returned more than 10000 results. Try with this block range [0x451e459, 0x45411b0].” (zkSync Era, capital Q).
  • -32602: “query exceeds max results 20000, retry with the range 53027049-53027329” (publicnode on Base and BNB, dRPC, Plasma). The hint is decimal here, hex above.
  • -32200: “query exceeds max results 200000, retry with the range 35392697-35480650” (Scroll, Optimism).
  • -32000: “logs matched by query exceeds limit of 10000” (Arbitrum) and “too many results, consider narrowing your query criteria, the limit is 5000” (Sonic). No hint.

What to do: shrink the window, and if the message carries a range, use it. Add an address or more topics if you can. Do not make the limit permanent, because the number of logs per block changes with traffic. The “10000” in “limit of 10000” is a number of logs, not blocks.

Response size

  • -32003: a “response too large” message that gives the response size (52432024 bytes against a 52428800 byte limit), asks for a block range of at most 481 blocks and repeats how many you asked for (963) (rpc-bnb.blockmachine.io). We paraphrase it because the live string uses a dash.
  • -32020 (HTTP 400): “backend response too large” (Optimism public endpoint). No hint.

What to do: the same as a result count error. Note that the 481-block hint came when 962 blocks had just passed, so hints can be conservative.

Block range: the window itself is too wide

  • -32602: “range 10001 exceeds limit of 10000” (Linea).
  • -32602 (HTTP 400): “block range greater than 10000 max” (Mantle, Unichain).
  • -32602: “block range too large: total block range in this request exceeds the maximum of 100 blocks” (Avalanche).
  • -32062: “Block range is too large” (rpc3.monad.xyz), and -32602 “block range too large” on rpc1.monad.xyz.
  • -32614 (HTTP 413): “eth_getLogs is limited to a 10,000 range” (Plasma, some Optimism backends). The same string says “a 100 range” on Monad.
  • -32701: “exceed maximum block range: 50000” (publicnode: 10000 on Polygon, 50000 on Base, Avalanche, Scroll, Sonic and Mantle).
  • -32602: “query spans 10000006 blocks (507834394 to 517834399), but only 10000000 are allowed for this request; narrow the block range” (Arbitrum). Without an address it ends “or add an address filter” and the limit is 30000.
  • -32600 (HTTP 400): “You can make eth_getLogs requests with up to a 10 block range. Based on your parameters, this block range should work: [0x190bb90, 0x190bb99]” (blastapi public hosts).
  • -32000: “log query range must not exceed 25 blocks” (bsc.blockrazor.xyz, window 26 passed, 27 failed).
  • Code 35 (HTTP 400): “ranges over 10000 blocks are not supported on free plan” (dRPC free tier). The number in the message is wrong: the cutoff was a window of 101.

What to do: shrink to the stated number, or halve if the number cannot be trusted. A block range limit does not move with traffic, so remember it for the rest of the run.

Start too far behind the head

This is not a width problem. The window can be one block and still fail. Move the start closer to the head, or use an endpoint with archive access.

Address required

  • -32701: “Please specify an address in your request or, to remove restrictions, order a dedicated full node here: https://www.allnodes.com/eth/host” (publicnode, for anything beyond one block without an address).

Method refused

  • -32005: “limit exceeded” (the BNB Chain dataseeds, even for one block).
  • -32000: “The method eth_getLogs is not supported.” (eth.meowrpc.com and its BSC, Base and Arbitrum siblings).
  • -32011 (HTTP 429): “request limit reached” (mainnet.base.org, for eth_getLogs only).

What to do: stop narrowing. A one-block call that fails with the same message means the endpoint will not serve this method for you at any window. Use a different endpoint. Retrying does nothing.

Rate limits

  • Code 15 (HTTP 429): “You reached Public endpoint rate limit, please upgrade to paid plan” (dRPC free).

What to do: back off and retry the same window. Our guides on rate limits and handling RPC errors cover the retry shape.

Same code, different meaning

Three codes made this clear:

  • -32005 is a result count on Linea and MEV Blocker and a method refusal on the BNB dataseeds.
  • -32602 is a block range on Linea, Mantle and Avalanche. It is a result count on zkSync Era and dRPC, and on publicnode it means either a result count or “start too far back”.
  • -32000 is a result count on Arbitrum and Sonic, a block range on bsc.blockrazor.xyz and a disabled method on meowrpc.

The portable signals are the message text and one test: does a one-block call work?

What the providers’ documentation says

We read these pages on 24 October 2026. Some of them differ from each other, and the dRPC one differs from what we measured.

  • Alchemy gives three different limits across its pages. The eth_getLogs reference lists Ethereum, Base, Optimism, Arbitrum, Polygon, BNB and others at 10 blocks on Free and “unlimited” on Pay As You Go and Enterprise, with a 150MB response cap. A support article says that on most major chains a Pay As You Go request succeeds if it stays within a 10,000 block range with no cap on response size, or spans a wider range with 10,000 logs or fewer. The deep-dive guide, for Ethereum, says any range with a cap of 10K logs, or a 2K block range with no cap on logs. That is unlimited, a 10,000-block range and a 2K-block range. We could not test Alchemy’s own endpoints, so we cannot say which one holds.
  • The “should work” hint. The blastapi.io hosts return a hint worded “this block range should work: [0x190bb90, 0x190bb99]”. We found no provider documentation that describes this wording, so parse it when it appears, but do not rely on it being documented.
  • dRPC documents “The log limit is capped at 10,000 entries” and a 2 second maximum timeout, and gives no block range number. Measured on the free tier: a result cap of 20,000, and a block cutoff at a window of 101 (range 100) behind a message that says 10,000. Our Ethereum network page measured 101 passed and 105 failed on 22 October, and the 24 October re-test agrees on 101.
  • Infura (MetaMask Services) documents three constraints: 5,000 parameters per request, 10,000 results and a 10 second query duration. It states no block range. The errors it shows are -32005 “query returned more than 10000 results” and -32005 “query timeout exceeded”.
  • QuickNode documents 5 blocks on the Free Trial plan and 10,000 on paid plans. Its support article shows the error -32602 “eth_getLogs and eth_newFilter are limited to a 10,000 blocks range” and adds that this is not a limit on how far back you can query. That is a block-range limit of 10,000 under -32602, against Infura’s 10,000-result cap under -32005.
  • Chainstack documents 100 blocks on the Developer plan and 10,000 on Growth, Pro, Business and Enterprise, and adds that some chains enforce hard limits regardless of plan.
  • Ankr documents a maximum block range of 1,000 for Public, 3,000 for Freemium and 10,000 for Premium, 200,000 logs per response, and 100 for eth_getLogs on Monad on the public tier. We could not test it: rpc.ankr.com/eth answered “Unauthorized” from our connection.

Paid and keyed behaviour was not tested for any provider. The lesson is practical: a documented number is a starting point, and the first error you get is the real answer.

BNB Smart Chain

BNB Smart Chain is where eth_getLogs goes wrong most often, and each failure is different.

  • The official dataseeds refuse the method. BNB Chain’s documentation says eth_getLogs is disabled on its listed mainnet endpoints. We called six of the seven hosts on that list (bsc-dataseed.bnbchain.org, bsc-dataseed.nariox.org, bsc-dataseed.defibit.io, bsc-dataseed.ninicoin.io, bsc.nodereal.io and bsc-dataseed-public.bnbchain.org) with a single block and the USDT address. All six answered -32005 “limit exceeded” at 12:38:49 UTC. Three older dataseed hostnames (bsc-dataseed1.binance.org, bsc-dataseed1.defibit.io, bsc-dataseed1.ninicoin.io) did the same on all three filters. On each host eth_blockNumber worked in the same session, so the host was up and only the method was off.
  • The seventh listed host served logs. rpc-bnb.blockmachine.io is on the same documentation list as disabled, but a one-block call returned logs. Its narrow window cap is 10,000 (window 10,001 failed with -32602, “block range exceeds maximum allowed (max=10000, requested=10001)”). On a busy USDT filter a 50 MiB response cap bites first: 962 blocks passed (82,449 logs) and 963 failed with -32003 in the re-run (943 and 944 in the first run).
  • publicnode cuts at 20,000 results. bsc-rpc.publicnode.com has the same start-depth rule as Ethereum. On USDT, 222 blocks passed (19,934 logs) and 223 failed with -32602 “query exceeds max results 20000” in the re-run (237 and 243 in the first run). That is about 220 to 240 blocks, and without an address only one block works.
  • Others. bsc.blockrazor.xyz allows a window of 26 and fails at 27. rpc-bsc.48.club was slow rather than limited: windows of 275 to 450 blocks took about 30 seconds, and several hit our timeout.

Block time makes the block counts matter even more. At the measured 0.450 seconds per block, 10,000 blocks is 1.25 hours. If a guide tells you to start with a few thousand blocks on BNB Smart Chain, check it against the result cap of the endpoint you use, because busy contracts hit that first. Our BSC endpoint page records the dataseed finding from 22 October, and the 24 October re-test matched it.

Oddities worth knowing

  • mainnet.base.org refuses eth_getLogs and nothing else. It answered 429, -32011, “request limit reached” for one block with an address, without one and for latest, at 12:13, 12:39 and 12:41 UTC. A lone call after minutes of idle carried the headers x-ratelimit-limit: 5, 5;w=1 and x-ratelimit-remaining: 0. On the same host eth_blockNumber, eth_getBlockByNumber and eth_getBlockReceipts returned 200. It looks like a rate limit and behaves like a disabled method, so retrying will not help.
  • mainnet.optimism.io is nondeterministic. The same request (a 50,000-block window, address, Transfer topic and a random sender) sent 16 times in a row passed 12 times with zero logs and failed 4 times with -32614 “eth_getLogs is limited to a 10,000 range”. The claimed 10,000 cap exists on some backends only. A ladder run at 12:27 UTC had window 10,002 fail, 20,000 and 50,000 pass, 96,000 fail and 100,000 and 200,000 pass. With the USDC address, a 1,000-block window passed (5,519 logs in 10.5 s), 5,000, 10,001 and 20,000 returned -32020 “backend response too large”, and 50,000 and above returned -32200 with a 200,000-result cap. Any limit you learn from one failure here can be gone on the next call. We did not identify the cause.
  • publicnode’s busy-filter limit on Ethereum moves. In the first run the same request passed at 2,515 blocks (247,568 logs in 23 s) and timed out at 2,562 (12:13:45). In the re-run a 2,000-block window returned about 215,900 logs in 8.4 s and again in 8.7 s. The start-depth rule, about 10,000 blocks behind the head, held in every run.
  • Some hosts would not serve us. polygon-rpc.com returned HTTP 401 with “API key disabled, reason: tenant disabled” at 12:55:44 UTC, even for eth_chainId. The re-run matched. eth.llamarpc.com returned HTTP 525. 1rpc.io answered HTTP 200 with a -32001 error, “You’ve reached the usage limit for your current plan”, on eth_chainId; that message is specific to a plan or an address, so it reads like a block on our connection, not an outage. monad-rpc.publicnode.com returned 404 with an empty body. A control call to ethereum-rpc.publicnode.com answered normally at the same time. If an endpoint list in a guide is older than a few weeks, test it.
  • rpc.flashbots.net states a block range cap of 100,000. A window of 100,001 blocks (a range of 100,000) passed in 3 to 5 seconds, and windows of 100,002 and above were refused in under a second with 200, -32602, “query exceeds max block range 100000”. One parallel run answered 200 with code 4444, “pruned history unavailable”, at 100,000 blocks. Its busy filters ran into our 30 second timeout: a 734-block window with 77,326 logs passed in 30.4 s in the first run and timed out in the re-run.

Probe an endpoint yourself

The same method works for any endpoint you depend on:

  1. Send eth_blockNumber. If that fails, the host is the problem.
  2. Send eth_getLogs for one block with your real filter. If this fails with the same message you get at larger windows, the method is refused. Stop.
  3. Climb a ladder with a narrow filter that returns zero logs (your token, the event topic and a random address as a second topic): 1, 10, 100, 500, 1,000, 2,000, 5,000, 10,000, 20,000, 50,000, 100,000. Bisect between the last pass and the first failure. This is the block range rule, free of result caps.
  4. Repeat with your real filter and check the log count is above zero. The first failure here is usually the result cap, and the number of logs per block decides it.
  5. Send a 10-block window that ends 100, 1,000 and 10,000 blocks behind the head. If those fail where the head works, you have a depth rule, not a width rule.

Run the probe at a quiet time, and more than once. Several endpoints above changed their answer between calls.

Adaptive helpers that handle all of this

A fixed chunk size cannot survive the spread above. The approach that did survive every endpoint we ran it against is adaptive:

  1. Start at 2,000 blocks and double after each success.
  2. On an error, read the text. If the method is off or a key is needed, throw and do not retry. If it is a rate limit, back off and retry the same window.
  3. Otherwise shrink. If the message has a hint, use it: a hex pair [0x..., 0x...], a decimal range A-B, narrow to <= N blocks, or for range errors a stated maximum like “limited to a 10,000 range”. With no hint, halve.
  4. A range error sets a ceiling for the rest of the run. A result count error does not, because density changes block to block. After one, aim 20 percent under the hint and stop doubling.
  5. If a one-block window fails, give up with a clear message.

Two bugs came out of testing, and both are worth knowing:

  • “Limit of 10000” in logs matched by query exceeds limit of 10000 is a number of logs. The first version used it as a window and looped more than 60 times, shrinking by one block each time. The fix is to trust a stated maximum only on range errors.
  • dRPC’s “ranges over 10000 blocks are not supported on free plan” contains the words “not supported”. A naive check for an unsupported method aborts. The real cutoff there is 101, and halving is the right answer: 2,000, 1,000, 500, 250, 125, 62, which passes.

An earlier version of the chunker in our production pipeline guide halved only when the message contained “block range” or “too many”. Run verbatim at its default chunk size of 2,000, it threw on dRPC and publicnode Base. With a chunk size of 20,000 it threw on Linea and Plasma too. The guide now uses the helper below.

ethers v6

// Everything a provider says about a failure ends up somewhere in the error.
// ethers v6 puts it in different fields depending on the HTTP status.
function bodyMessage(body) {
  try { return JSON.parse(body)?.error?.message; } catch { return undefined; }
}
function errorText(err) {
  return [err?.error?.message, err?.info?.error?.message, bodyMessage(err?.info?.responseBody), err?.info?.responseBody, err?.shortMessage]
    .filter(Boolean)
    .join(' | ');
}

// How big a window does the provider say would have worked?
// Returns a block count, or null when the message carries no hint.
function suggestedWindow(text) {
  // Hex pairs (Linea, MEV Blocker, blastapi):
  //   "Try with this block range [0x1EE7816, 0x1EE908A]"
  //   "this block range should work: [0x190bb90, 0x190bb99]"
  let m = text.match(/\[\s*(0x[0-9a-f]+)\s*,\s*(0x[0-9a-f]+)\s*\]/i);
  if (m) return Number(BigInt(m[2]) - BigInt(m[1])) + 1;
  // Decimal pair: "retry with the range 53027049-53027329"
  m = text.match(/range\s+(\d+)\s*-\s*(\d+)/i);
  if (m) return Number(m[2]) - Number(m[1]) + 1;
  // "narrow to <= 73541 blocks for this filter"
  m = text.match(/narrow to <=\s*(\d+)\s*blocks/i);
  if (m) return Number(m[1]);
  return null;
}

// A stated maximum, when there is one: "limited to a 10,000 range", "exceed maximum block range: 50000"
function statedMax(text) {
  const m = text.match(/(?:limited to a|exceed maximum block range:|must not exceed|up to a|exceeds the maximum of|exceeds limit of)\s*([\d,]+)/i);
  return m ? Number(m[1].replace(/,/g, '')) : null;
}

const FATAL = /method .*not supported|is disabled|archive requests require|specify an address|unauthori[sz]ed|api key/i;
const RATE = /rate.?limit|too many requests|request limit reached|usage limit|429/i;
const SIZE = /more than \d+ (?:results|bytes)|exceeds max results|too many results|logs matched by query exceeds|response (?:size )?(?:too large|exceeded)|query timeout|timed out|timeout/i;

export async function getLogsAdaptive(provider, filter, fromBlock, toBlock, opts = {}) {
  const { start = 2000, max = 100_000, onStep = () => {} } = opts;
  const out = [];
  let from = fromBlock;
  let window = start;
  let rangeCeiling = max; // learned from range errors: stays
  let grow = 2;           // doubles until a result-count error, then creeps
  let rateRetries = 0;

  while (from <= toBlock) {
    const to = Math.min(from + window - 1, toBlock);
    try {
      const logs = await provider.getLogs({ ...filter, fromBlock: from, toBlock: to });
      onStep({ from, to, window, ok: true, logs: logs.length });
      out.push(...logs);
      from = to + 1;
      rateRetries = 0;
      window = Math.min(Math.ceil(window * grow), rangeCeiling); // grow after a success
    } catch (err) {
      const text = errorText(err);
      if (FATAL.test(text)) throw new Error(`not retryable: ${text.split(' | ')[0]}`);
      if (RATE.test(text) && !SIZE.test(text)) {
        if (++rateRetries > 5) throw new Error(`rate limited: ${text}`);
        const wait = 500 * 2 ** rateRetries;
        onStep({ from, to, window, ok: false, why: `rate limit, wait ${wait}ms` });
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }
      if (window === 1) throw new Error(`a single block fails, so the endpoint cannot serve this filter: ${text.split(' | ')[0]}`);

      const isResultError = SIZE.test(text);
      // A stated maximum is only a block count on range errors; on result errors it is a log count ("limit of 10000").
      const hint = suggestedWindow(text) ?? (isResultError ? null : statedMax(text));
      // A hint is the biggest window that fit this time; aim 20% under it for result errors, because density moves.
      const target = hint === null ? Math.floor(window / 2) : isResultError ? Math.floor(hint * 0.8) : hint;
      const next = Math.max(1, Math.min(target, window - 1));
      if (isResultError) grow = 1.1; // dense data: stop doubling into the same wall
      if (!isResultError) rangeCeiling = Math.min(rangeCeiling, next); // a range limit does not move with traffic
      onStep({ from, to, window, ok: false, why: text.split(' | ')[0].slice(0, 90), next, hinted: hint !== null });
      window = next;
    }
  }
  return out;
}

Call it like this (this is how we ran every test; batchMaxCount: 1 keeps ethers from batching, because some free tiers limit batches):

import { JsonRpcProvider, id } from 'ethers';

const provider = new JsonRpcProvider(url, undefined, { staticNetwork: true, batchMaxCount: 1 });
const filter = { address: token, topics: [id('Transfer(address,address,uint256)')] };
const logs = await getLogsAdaptive(provider, filter, fromBlock, toBlock, {
  onStep: (s) => console.log(s),
});

web3.py v8

import math
import re
import time

from web3 import Web3

TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"  # keccak("Transfer(address,address,uint256)")

FATAL = re.compile(r"method .*not supported|is disabled|archive requests require|specify an address|unauthori[sz]ed|api key", re.I)
RATE = re.compile(r"rate.?limit|too many requests|request limit reached|usage limit|429", re.I)
SIZE = re.compile(
    r"more than \d+ (?:results|bytes)|exceeds max results|too many results|logs matched by query exceeds|"
    r"response (?:size )?(?:too large|exceeded)|query timeout|timed out|timeout",
    re.I,
)


def error_text(exc):
    """Collect everything the failure says: RPC error body, HTTP body, exception string."""
    parts = []
    resp = getattr(exc, "rpc_response", None)  # web3.exceptions.Web3RPCError
    if isinstance(resp, dict) and resp.get("error"):
        parts.append(str(resp["error"].get("message", "")))
    http = getattr(exc, "response", None)  # requests.HTTPError
    if http is not None:
        try:
            parts.append(str(http.json()["error"]["message"]))  # JSON-RPC error inside a 4xx/5xx body
        except (ValueError, KeyError, TypeError):
            parts.append(http.text[:300])
    parts.append(str(exc))
    return " | ".join(p for p in parts if p)


def suggested_window(text):
    """Block count the provider says would have worked, or None."""
    m = re.search(r"\[\s*(0x[0-9a-f]+)\s*,\s*(0x[0-9a-f]+)\s*\]", text, re.I)  # hex pair (Linea, MEV Blocker, blastapi)
    if m:
        return int(m.group(2), 16) - int(m.group(1), 16) + 1
    m = re.search(r"range\s+(\d+)\s*-\s*(\d+)", text, re.I)  # "retry with the range 100-200"
    if m:
        return int(m.group(2)) - int(m.group(1)) + 1
    m = re.search(r"narrow to <=\s*(\d+)\s*blocks", text, re.I)  # BoltRPC gateway
    if m:
        return int(m.group(1))
    return None


def stated_max(text):
    m = re.search(
        r"(?:limited to a|exceed maximum block range:|must not exceed|up to a|exceeds the maximum of|exceeds limit of)\s*([\d,]+)",
        text,
        re.I,
    )
    return int(m.group(1).replace(",", "")) if m else None


def get_logs_adaptive(w3, flt, from_block, to_block, start=2000, max_window=100_000, on_step=lambda s: None):
    out = []
    cur, window = from_block, start
    range_ceiling, grow, rate_retries = max_window, 2.0, 0
    while cur <= to_block:
        end = min(cur + window - 1, to_block)
        try:
            logs = w3.eth.get_logs({**flt, "fromBlock": cur, "toBlock": end})
            on_step({"from": cur, "to": end, "window": window, "ok": True, "logs": len(logs)})
            out.extend(logs)
            cur, rate_retries = end + 1, 0
            window = min(math.ceil(window * grow), range_ceiling)  # grow after a success
        except Exception as exc:  # noqa: BLE001 - providers raise a mix of Web3RPCError and HTTPError
            text = error_text(exc)
            if FATAL.search(text):
                raise RuntimeError(f"not retryable: {text.split(' | ')[0]}") from exc
            if RATE.search(text) and not SIZE.search(text):
                rate_retries += 1
                if rate_retries > 5:
                    raise RuntimeError(f"rate limited: {text}") from exc
                wait = 0.5 * 2**rate_retries
                on_step({"from": cur, "to": end, "window": window, "ok": False, "why": f"rate limit, wait {wait}s"})
                time.sleep(wait)
                continue
            if window == 1:
                raise RuntimeError(f"a single block fails, so the endpoint cannot serve this filter: {text.split(' | ')[0]}") from exc
            is_result_error = bool(SIZE.search(text))
            # A stated maximum is only a block count on range errors; on result errors it is a log count ("limit of 10000").
            hint = suggested_window(text) or (None if is_result_error else stated_max(text))
            # A hint is the biggest window that fit this time; aim 20% under it for result errors, because density moves.
            target = window // 2 if hint is None else (int(hint * 0.8) if is_result_error else hint)
            nxt = max(1, min(target, window - 1))
            if is_result_error:
                grow = 1.1  # dense data: stop doubling into the same wall
            else:
                range_ceiling = min(range_ceiling, nxt)  # a range limit does not move with traffic
            on_step({"from": cur, "to": end, "window": window, "ok": False, "why": text.split(" | ")[0][:90],
                     "next": nxt, "hinted": hint is not None})
            window = nxt
    return out

Call it with w3 = Web3(Web3.HTTPProvider(url, request_kwargs={"timeout": 30})) and a filter such as {"address": Web3.to_checksum_address(token), "topics": [TRANSFER]}. The Web3RPCError branch covers HTTP 200 errors and the requests.HTTPError branch covers 4xx and 5xx bodies, which is why error_text reads both.

What the helpers did

Node with ethers 6.17.0 and Python 3.12.10 with web3 8.0.0, public endpoints only, 24 October 2026:

  • Linea: 40,000 blocks, 7 calls, 1 failed. The window doubled to 16,000, failed with “range 15999 exceeds limit of 10000” and dropped to 10,000 from the message. 18,730 logs.
  • MEV Blocker (USDT, hex hint): 25 calls, 5 failed, 168,742 logs. A failure at 88 blocks took the next window to 60.
  • publicnode on Base (USDC, decimal hint): 18 calls, 5 failed, 211,623 logs. It went from 2,000 blocks to 268 on the first failure and to 254 after a later one.
  • blastapi Ethereum (“should work” hint): 5 calls, 1 failed, straight from 2,000 to 10.
  • dRPC Ethereum: 15 calls, 5 failed. The misleading message made it halve five times to 62 blocks, then pass ten times at that size. 59,315 logs.
  • Sonic (5,000-result cap, no hint): halved from 16,000 to 8,000 to 4,000, 11 calls, 2 failed.
  • Arbitrum One: halved once from 32,000 to 16,000 after “logs matched by query exceeds limit of 10000”, 7 calls, 1 failed.
  • Monad (100 range) and Plasma (10,000 range): one failure each, then the stated maximum.
  • BNB dataseed: halved from 2,000 down to 3 blocks, then 1, then stopped with “a single block fails, so the endpoint cannot serve this filter: limit exceeded”.
  • meowrpc: stopped at once with “not retryable: The method eth_getLogs is not supported.”

Python ran against Plasma, Arbitrum One, MEV Blocker, blastapi, dRPC, zkSync Era, publicnode on Base (decimal hint), the BNB dataseed and meowrpc, with the same behaviour. Call counts and log totals on busy filters depend on the span and on traffic: a later run on publicnode Base took 17 calls with 4 failed.

Branches we barely exercised

  • The narrow to <= N blocks hint, which comes from BoltRPC’s gateway. We never ran the helpers against BoltRPC. We fed the message in the next section to suggestedWindow and it returns 4914. The same message needed one change in SIZE: the live gateway counts bytes (“more than 268435456 bytes”), so the pattern accepts results or bytes. Without that, the helper would treat the refusal as a block range limit.
  • The rate limit retry branch. One endpoint, mainnet.base.org, sent the helper into it: the helper waited 1000 ms and then 2000 ms. ethers also retries a 429 on its own, so each attempt took minutes and we stopped the run after about nine minutes. We did not see that branch finish.

Two caveats in the code. RATE matches the bare string “429”, so any message containing that number reads as a rate limit, including a hex hint such as [0x1429ab, 0x1439cd]. It also matches “request limit reached”, which is what mainnet.base.org says when it refuses eth_getLogs, so the helper retries that window five times before it gives up. FATAL includes “specify an address”, which only publicnode uses. Change them to suit your providers.

What BoltRPC does with a wide eth_getLogs

The limits below come from our docs, plus one live measurement. The public endpoint measurements earlier in this article did not touch BoltRPC.

  • No block-range cap. A wide range is split into chunks and fanned out across the chain’s nodes server-side, then merged into one answer and billed as one request (4 RU).
  • The practical limits are response size and a 60 second deadline. Past the size limit, the request is refused with an error that names a narrower range. Our docs page says a single request returns at most 250,000 logs. The live gateway states its limit in bytes instead (268,435,456 bytes in the message below). We have not reconciled the two, so read each as what it says. Set your client timeout to at least 60 seconds, the per-request deadline in the docs.
  • History floor. A range that begins below what any node stores is refused with -32000 and data.earliestBlock naming the first stored block. It is never clamped silently, so you do not get a partial answer that looks complete.

We measured the gateway live on 24 October 2026 at 13:42 UTC, on Ethereum with a USDC Transfer filter:

  • 100,001 blocks was refused with -32005.
  • 2,000 blocks returned 156,606 logs (99.5 MB) in one call; our client saw the full answer after 75 seconds, which includes downloading the 99.5 MB response.
  • A retry of 4,900 blocks, inside the hint below, timed out with -32002 “request timed out” after 30 seconds.
  • A narrow filter over 1,000,000 blocks answered [] in 0.5 seconds. Over 5,000,000 blocks it hit a 504, “eth_getLogs did not complete within the 60000 ms deadline”.

The refusal for the 100,001-block call looked like this:

query returned more than 268435456 bytes (~4689 MB projected for 100001 blocks; narrow to <= 4914 blocks for this filter)

That is the shape the helper above parses: the narrow to <= 4914 blocks part gives the next window. The hint is an estimate from the filter’s density, and the 4,900-block retry shows it can still be too big, so treat it as a ceiling, not a target. The helper aims 20 percent under it for this kind of error. You can see the chains we serve on the networks page.

FAQ

What is the eth_getLogs block range limit?

There is no standard limit. On 24 October 2026 public endpoints accepted anything from a 10-block window to no block cap at all on a filter that matches nothing. On busy contracts a result cap of 5,000 to 200,000 logs usually bites first, so the practical limit depends on your filter, not only on your block range.

What does “query returned more than 10000 results” mean?

The block range was accepted, but the matching logs exceed the endpoint’s result cap. Narrow the window, add an address or more topics, or follow the block range in the message if it gives one. The cap is on logs, so the right window size changes with how busy the contract is.

Why does eth_getLogs return -32005 “limit exceeded” on BNB Smart Chain?

The official BNB Chain dataseeds have eth_getLogs turned off, and they answer -32005 “limit exceeded” even for a single block. Narrowing the range will not help. Use an endpoint from a provider that serves logs, and chunk by result size because busy contracts hit a result cap before any block limit.

Does -32005 always mean the same thing?

No. It is a result count error on Linea and MEV Blocker, and Infura documents it that way. On the BNB dataseeds it means the method is refused. Other codes overlap too: -32602 and -32000 each cover both block range and result count errors. Read the message, and test whether a one-block call works.

Does BoltRPC cap the eth_getLogs block range?

No. BoltRPC has no hard block-range cap. A wide range is split and fanned out server-side and billed as one request. The limits that apply are the response size and a 60 second deadline. Our docs page says a single request returns at most 250,000 logs, while the live gateway stated its limit in bytes when we tested it. Past the limit the request is refused with an error that names a narrower range, which is an estimate, so treat it as a ceiling.

Next steps

Once chunking works, the rest of an indexer is checkpoints, reorg handling and storage, which the production eth_getLogs pipeline covers. To run these helpers against an endpoint with no block-range cap, create a key and point them at it.

Start your free 14-day trial

Questions we get about this.

Content is for informational purposes only and does not constitute financial, legal or technical advice. Code examples and configurations are provided as-is. Verify against official documentation and test in your own environment before deploying to production.

More from the blog.

All articles →
Pricing

Alchemy Alternative: What Switching Really Costs

Switching from Alchemy pays off for standard JSON-RPC at volume. A worked 50M-call bill, what you give up and the migration steps.

Oct 24, 2026 · 18 min read
RPC

Migrating from Grove Portal: A Practical RPC Move

Grove has wound down and its rpc.grove.city endpoints no longer resolve. Map your endpoints, check chain coverage and move a production app without a rewrite.

Oct 24, 2026 · 7 min read
RPC

Monad RPC Limits: eth_getLogs, Gas Caps, WebSocket

Monad RPC limits measured on 24 October 2026: eth_getLogs spans per public host, the 150M gas cap, batch limits, finality and monadNewHeads commit states.

Oct 24, 2026 · 16 min read