You pick a 2,000-block eth_getLogs window because it worked on Ethereum. You point the same indexer at Arbitrum and finish a day of history in a handful of calls. You point it at a free-tier Alchemy key on Base and every call dies at 10 blocks. The filter object did not change. The range limit did: plan ceiling, client default, and block time stacked into a different wall-clock job.
This is the operational companion to the eth_getLogs caps map (client defaults, result-count caps, chunking code) and to archive-cost writeups that treat historical logs as a billing problem. Here the question is narrower: what block span do you start with on each hub this directory lists, and how do providers signal that your span is wrong?
This is not a ranking of endpoints. Open the chain hub, compare public and paid rows, then probe the URL you will ship.
Range is not one number
Three different things get called a "range limit":
- Plan or gateway span —
toBlock - fromBlock + 1above a documented ceiling (QuickNode paid 10,000; Alchemy free 10; Ankr public often 1,000). - Client span — Reth's
--rpc.max-blocks-per-filterdefault of 100,000, Geth's optional--rpc.rangelimit(off by default). - Effective history per call — the same 10,000-block cap is ~33 hours on Ethereum (~12s) and ~42 minutes on Arbitrum (~250ms).
Result-count and timeout caps still fire inside a legal span. If you only shrink on "range too large," you will loop when Infura returns -32005 for 10,000 results or a public gateway returns an empty 504. Treat range, result count, and timeout as separate failure classes. The caps guide walks that taxonomy. This page sizes the first class.
Starting chunk sizes by hub
Use these as first probes with address and topics[0] set. Empty filters on public hubs time out before the span math matters. Halve on range errors. Do not halve below 1 block on result-count errors — split by address or topic instead.
| Hub | Approx. block time | Start span (blocks) | Wall-clock per chunk (order of) | Why that start |
|---|---|---|---|---|
| Ethereum | ~12s | 2,000 | ~6.5h | Survives most paid 10k ceilings; leaves room to grow |
| Gnosis | ~5s | 2,000 | ~2.8h | Same numeric start as Ethereum; less wall-clock |
| Base | ~2s | 5,000 | ~2.8h | ~2s L2; paid plans often allow 10k |
| Optimism | ~2s | 5,000 | ~2.8h | Same OP-Stack ballpark as Base |
| Polygon | ~2s | 2,000 | ~1.1h | Some providers publish tighter Polygon exceptions (e.g. Ankr) |
| Linea | ~2s | 5,000 | ~2.8h | Treat like other ~2s EVMs until the endpoint says otherwise |
| Mantle | ~2s | 5,000 | ~2.8h | Same |
| Avalanche C-Chain | ~2s | 5,000 | ~2.8h | C-Chain only; path and gas differ, log method does not |
| BNB Smart Chain | sub-second–~1.5s | 2,000–3,000 | ~1h or less | Dense chain; start conservative on public rows |
| Arbitrum | ~250ms | 1,000 | ~4 min | Fast blocks burn a 10k cap in ~42 min; small chunks avoid timeouts |
| opBNB | fast L2 | 2,000 | minutes | Parent is BSC; probe before you copy a Base size |
| Fantom / Sonic | sub-second–~1s | 2,000 | minutes | Do not import an Ethereum-only chunk size |
| Zora | ~2s | 5,000 | ~2.8h | OP-Stack-shaped; confirm on the hub endpoint |
| Chiliz | chain-specific | 2,000 | measure | Same method, different cadence |
| Moonbeam / Moonriver (Frontier) | ~6s | 2,000 | ~3.3h | Substrate hubs that still speak eth_getLogs |
How to read the table. The start span is a smoke-test default for a filtered query on a paid or dedicated URL. Public hub URLs are for a 1-block or tiny-range filter check, not a backfill. If the provider row documents a lower ceiling (Alchemy free = 10 blocks), that ceiling wins.
Convert a wall-clock goal to blocks, not the other way around:
blocks_for_one_hour ≈ 3600 / block_time_seconds
One hour on Ethereum is ~300 blocks. One hour on Arbitrum is ~14,400 blocks — already above many paid 10,000-block caps. A "scan the last day" job is a few requests on Ethereum and dozens on Nitro. Size the RPC call in blocks. Use wall-clock only for job ETA.
Same filter, different provider errors
Copy one filter. Change only the host. The failure mode tells you which knob to turn.
| What you see | Likely cause | First fix |
|---|---|---|
-32602 … limited to a 10,000 blocks range (QuickNode-style) | Hard plan span | Cap chunk at ≤10,000; do not retry the same span |
-32005 … query returned more than 10000 results (Infura-style) | Result count, not span | Add address / topics; split contracts; do not only halve |
-32005 … query timeout exceeded | Duration cap (~10s on Infura) | Shrink span and tighten filter |
| Free-tier / trial tiny spans (Alchemy 10, QuickNode trial 5) | Plan gate | Expect it; do not treat as a node bug |
| Ankr (and similar) per-chain exceptions | Published chain override | Read that chain's row; Polygon has been tighter than "EVM default" |
| HTTP 429 / empty body / HTML 504 | Gateway or public shared node | Sleep, halve, or move off the public URL |
[] with no error on a deep historical range | Pruned / full node without the log index you need | Archive (or receipt-scan) problem — not a range-cap problem |
Alchemy's paid major-chain story in 2026 is the surprise inverse: unlimited span on Ethereum, Base, Optimism, Arbitrum, Polygon, BNB, with a 150MB body cap. Free tier stays at 10 blocks. A script that "worked on the team key" and fails on a demo key is usually that gate.
Infura is the opposite shape: no fixed span in the docs, but 10,000 results, 10 seconds, and 5,000 filter parameters. You can ask for a million blocks and still lose on density.
QuickNode paid 10,000 is a hard JSON-RPC error at 10,001. Chunk at the documented ceiling.
Dwellir's Ethereum method docs recommend staying in a 1,000–5,000 block window and filtering on topics so the node can reject at storage. In this repo's provider data, Dwellir bills 1 credit per response for eth_getLogs (flat), versus Infura 255 CU and Alchemy 60 CU for the same method. For log-heavy backfills, the pricing model matters as much as the span. Compare rows on the hub and on the pricing calculator.
Details and CU tables live in the caps guide. Use this page to pick the opening span per hub, then let errors steer.
Provider variance checklist (one filter, many hosts)
Before you hard-code a global MAX_SPAN = 2000:
- Open the hub for the chain you index (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Smart Chain, Avalanche, …).
- Note block time from the hub or chain docs. Compute how many blocks fit in the wall-clock window you care about.
- Check which providers list that network. Read that provider's getLogs / eth_getLogs range doc for the plan you pay for — free and paid differ.
- Smoke-test: 1-block filtered query on a public URL; then your start span from the table on the candidate paid URL.
- Persist the last successful
toBlock. Preferfinalizedorsafefor the cursor you store. Do not usependingforeth_getLogs. - For the live tip, prefer
eth_subscribe(logs) over pollingeth_getLogsevery block.
Public endpoints on the hubs are shared and rate-limited. They are fine to prove a filter. They are a poor primary for a multi-month backfill. When public rows keep timing out on logs, the public vs paid guide covers the upgrade criteria. Dwellir is one paid row with flat response credits and archive-oriented log guidance — useful when the mix is getLogs-heavy and CU multipliers make the bill opaque. It is not the only option. A cheaper URL that fails the probe is not a savings.
Range vs archive: do not confuse the two
A node can accept your span and still return [] for old blocks if receipts or log indexes were pruned. That is an archive-depth failure, not a range-cap failure. Shrinking the window will not restore missing history. Confirm archive (or a historical receipts path) when you backfill past the full-node retain window. The caps guide and archive-cost writeups cover silent empties; this page will not re-litigate them.
Conversely, an archive node can still reject a 50,000-block unfiltered query on plan span, result count, or timeout. Archive answers "how far back." Range limits answer "how wide per call."
Practical defaults you can ship
- Per-chain config — store
maxSpannext to the RPC URL and chain ID. Do not share one constant across Ethereum and Arbitrum. - Filtered first — every production call sets
addressand usuallytopics[0](event signature). Unfiltered scans are how public RPCs fall over during mints and airdrops. - Open low, grow on success — start at the table value; raise toward the provider ceiling only after a stretch of clean responses.
- Classify errors — range → shrink span; result count → tighten filter or split address; timeout/429 → shrink, sleep, or change host; empty historical
[]→ archive question. - Subscribe for head, getLogs for history — mixing those roles is how CU bills spike.
Browse the chain list for the EVM network you index, open its hub, and compare providers there. The method name is the same everywhere. The range that survives is not.