TriportRPC

eth_getLogs block range limits on Ethereum, BSC, Base, Polygon, TRON and Robinhood Chain

The block-range ceiling on eth_getLogs is not one number per chain — it can differ between nodes behind the same URL. What the six EVM-style chains we serve document, and a pager that survives all of them.

eth_getLogs is the method every EVM indexer leans on, and it is also the one that fails most unpredictably. The same filter that returns in one call on one chain comes back as -32602, -32005 or a generic -32000 on another — sometimes on the same chain, a minute later, because the request landed on a different node.

Provider references often answer the question per plan. Alchemy's eth_getLogs reference (read 2026-09-22) has a table headed "Chain | Free | Pay As You Go | Enterprise" that lists Ethereum, Base, Polygon, BNB and Robinhood Mainnet at 10 blocks on the free plan and "unlimited" above it, plus a 150MB response cap. What it does not cover is TRON, limits that differ between node classes inside one network, or nodes that refuse a filter without an address. Those are the cases that break an indexer behind a multi-node endpoint, so they are the subject here.

Why nodes cap the range at all

A log query is a scan. The node walks the bloom filters of every block in fromBlock..toBlock, reads the receipts of every block whose bloom might match, and builds one JSON response in memory. Two things grow with the range: the work per call and the size of the answer. A node operator can bound the first by capping the number of blocks and the second by capping the number of results. Different client software, configurations and hardware choose different caps, and a gateway that routes across several nodes inherits all of them.

That is the whole reason "what is the max range?" has no single answer. It is a property of the node that serves your call, not of the chain. See event logs and event topics in the glossary if you want the underlying data model first.

Per-chain, per-node limits

This is what each network's reference on Triport documents, plus one production check. The RPS column is the category the method is charged against, per plan (trial / Basic / Pro / Business); rows marked "configured" are limits we set in the plan matrix but have not load-measured.

NetworkDocumented range ceilingOther documented conditionError at the edgeBucket and RPS
EthereumNo fixed number published; page through "a few thousand blocks per call"-32602 for a malformed filterordinary read, 10 / 20 / 100 / 250
BNB Smart ChainPer node class: the limited class accepts 25 blocks and rejects 50; two standard classes accept 100 and reject 1,000One standard class requires address-32005 or a node error ("log query limit")heavy read, 15 / 20 / 100 / 250 (configured)
BasePer-node maximum ≤ 100 blocksaddress required by some nodes-32602 (the reference's listed error code)ordinary read, 10 / 20 / 100 / 250
Polygon"A maximum block span per call applies"; no number published-32602 per the reference; one production sample on 2026-09-22 answered a too-wide range with -32000 "invalid block range params"ordinary read, 15 / 20 / 100 / 250
TRON100 blocks, inclusiveHex quantities, 20-byte 0x addresses-32602 "requested block range exceeds the measured upstream limit"heavy read, 2 / 5 / 20 / 80 (configured)
Robinhood Chain"Not one number": limited differently on every serving channelDeep ranges may be rebuilt from block receipts-32005 for a range below the archive floorheavy read, 15 / 20 / 100 / 250 (configured)

Two rows deserve a comment.

BSC is the clearest example of limits that differ inside one network. A 60-block query succeeds on a standard node and fails on the limited class; an address-less query succeeds on two classes and fails on the third. The BSC reference says it directly: split wider queries before sending them "so routing can honor the selected node's capability". If you hard-code 100 because your first test passed, you will see intermittent failures that look like flakiness and are not.

TRON is included because its JSON-RPC layer exposes eth_getLogs over TRC-20 events, and its limit is the most explicit of the six. We re-checked it on production on 2026-09-22: a 256-block request came back as -32602 with the message above, consistent with the TRON reference. It is also the tightest budget in the table, at 2 requests per second during the trial, which is why the TRON USDT history walkthrough spends a whole section on scan arithmetic.

For Ethereum and Polygon we deliberately publish no number. A single successful wide query on one node on one day is not a limit, and quoting it would teach you to hard-code the one thing you should discover.

The address requirement

Some BSC and Base nodes refuse a log filter with no address. This is not a quirk to work around: an address-less filter is a scan of every contract's events, and the operator has decided not to run it. On the Base reference the schema note reads "Per-node maximum range is ≤ 100 blocks; address is required by some nodes." Both networks are covered on their landings, BSC RPC and Base RPC.

The practical rule: always send address, even when you are also filtering on topics. If you genuinely need events from any contract — for example every ERC-20 Transfer on a chain — you are building a chain-wide indexer, and the right tool is a block-by-block receipts read, not a wide log filter. eth_getBlockReceipts returns every receipt of one block, including all logs, and has no range to exceed.

Adaptive paging: shrink on a range error, grow on success

The pager below starts at a window you choose, halves it when the node rejects the range, and grows it slowly after a run of successes. It keeps three failure types apart, because they need different responses.

type Filter = { address: string | string[]; topics?: (string | string[] | null)[] };


async function scanLogs(rpc: (m: string, p: unknown[]) => Promise<any>,
                        filter: Filter, from: number, to: number) {
  let window = 100;          // a common documented ceiling; the pager shrinks it if needed
  const minWindow = 1;
  const logs: any[] = [];
  let cursor = from;
  let streak = 0;


  while (cursor <= to) {
    const end = Math.min(cursor + window - 1, to);
    try {
      const page = await rpc("eth_getLogs", [{
        ...filter,
        fromBlock: "0x" + cursor.toString(16),
        toBlock: "0x" + end.toString(16),
      }]);
      logs.push(...page);
      cursor = end + 1;                         // advance only after the page is stored
      if (++streak >= 5) { window = Math.min(window * 2, 2000); streak = 0; }
    } catch (e: any) {
      streak = 0;
      const kind = classify(e);
      if (kind === "range" || kind === "too_many_results") {
        if (window === minWindow) throw e;      // one block still fails: not a range problem
        window = Math.max(minWindow, Math.floor(window / 2));
      } else if (kind === "history") {
        throw e;                                // below the node's history: a smaller window won't help
      } else {
        throw e;                                // auth, rate limit, transport: handle elsewhere
      }
    }
  }
  return logs;
}

classify is where per-chain knowledge lives. Match on code and message, because the same code means different things on different chains: on BSC, -32000 with header not found means a missing block, while pruned-state text means unavailable history; on Robinhood Chain, -32005 for a range below the archive floor is a history failure, not a range one. The result-count error — "query returned more than N results" — is covered in its own page, query returned more than N results; it needs the same response (split the range), but its N also differs per node.

Two details matter in production. Store logs and advance the cursor only after the write commits, so a crash never skips a window. And deduplicate on (blockHash, transactionHash, logIndex): a window you retry after a timeout may already have been partly processed.

Range caps vs. the archive edge

A range error and a history error look similar in logs and have opposite fixes.

  • Range cap: the node can serve every block you asked for, just not that many in one call. Smaller windows succeed.
  • Archive edge: the node does not hold the blocks at all. Robinhood Chain documents a measured archive floor, and a query below it answers -32005 instead of an empty list; the archive depth page gives the current floor. Halving the window forever just burns your rate budget.

The pager above stops shrinking at one block and re-raises, which is what turns an archive failure into an alert instead of an infinite loop. An empty result is a third case, and not a failure at all: it means nothing matched, as long as the range was actually served. Our empty eth_getLogs result page walks through the reasons a filter that should match returns [].

Block hash queries and reorgs

eth_getLogs also accepts a single blockHash instead of a range, and the two cannot be combined. A hash query pins the answer to exactly one block, so it is immune to range caps and to reorgs: if that block is later orphaned, your query still describes it, and your reconciliation can drop its logs by hash. For the tip of the chain — the last blocks that can still reorganize — a practical pattern is range queries for history and per-block hash queries for recent blocks, with finalized data re-read once it is final.

Sizing the scan against your budget

Window size and rate budget multiply. A 100,000-block backfill of one contract costs 1,000 calls at a 100-block window and 4,000 calls at a 25-block window — the BSC limited-class size. On the BSC heavy-read bucket, Basic allows 20 requests per second sustained, so the difference is 50 seconds of budget versus 200 seconds, before retries. On TRON at 5 requests per second on Basic, the same 1,000 calls take 200 seconds of budget.

Two consequences follow. First, run backfills against the heavy bucket deliberately and away from your live-tip reads where they share a budget. Second, the adaptive pager earns its keep: a pager that settles on 100 where it can, instead of hard-coding 25 everywhere to be safe, uses a quarter of the calls. Burst capacity is twice the sustained rate on every plan, which absorbs a retry spike but not a sustained backfill; see rate limits and tiers for how 429 and Retry-After behave.

When this is the wrong approach

  • You need every event on the chain. Wide address-less filters are exactly what nodes refuse. Read receipts block by block, or use a purpose-built indexer.
  • You need native-coin transfers. A plain ETH value transfer emits no log — EIP-7708 states it plainly: logs "work for ERC-20 tokens, but they do not work for ETH" — so no window size will find it. That needs block and transaction scanning, not eth_getLogs.
  • You need a live feed. Polling ranges at the tip is a reasonable fallback, but where a logs subscription exists it is the better trigger, with eth_getLogs as the gap-fill after reconnects.
  • The range is below a node's history. No pager fixes missing data. Check the chain's retention before planning the backfill.

Sources

  1. Triport, eth_getLogs on Ethereum: block range or block hash, inclusive range, bounded windows, eth_read_rpc 10 / 20 / 100 / 250.
  2. Triport, eth_getLogs on BNB Smart Chain: 25/50 and 100/1,000 node classes, address required by one class, -32005, bsc_read_rpc_heavy 15 / 20 / 100 / 250 (default, unmeasured).
  3. Triport, eth_getLogs on Base: per-node maximum ≤ 100 blocks, address required by some nodes, base_read_rpc 10 / 20 / 100 / 250.
  4. Triport, eth_getLogs on Polygon: maximum block span per call, -32602, polygon_read_rpc 15 / 20 / 100 / 250.
  5. Triport, eth_getLogs on TRON: 100-block inclusive maximum, -32602, tron_read_rpc_heavy 2 / 5 / 20 / 80 (default, unmeasured). The 256-block rejection was re-checked on production on 2026-09-22.
  6. Triport, eth_getLogs on Robinhood Chain and archive depth: per-channel ceilings, receipts reconstruction, -32005 below the archive floor, robinhood_read_rpc_heavy 15 / 20 / 100 / 250 (default, unmeasured).
  7. Triport, rate limits and tiers: 2× burst, 429 with Retry-After.
  8. EIP-7708, "ETH transfers emit a log" (status Review) — https://eips.ethereum.org/EIPS/eip-7708, read 2026-09-22.
  9. Alchemy, eth_getLogs reference — https://www.alchemy.com/docs/reference/eth-getlogs, read 2026-09-22 (per-plan table and 150MB cap present; TRON, per-node classes and the address rule not found).