Data access · 8 min read
eth_getLogs: limits, pagination, and the logs it leaves out
eth_getLogs is the standard way to read events off an EVM chain, and it is the first wall most teams hit. Not because the method is hard to call, but because every endpoint caps it differently, history sits behind an archive node, and even when a query succeeds it can leave out a class of logs entirely. This guide shows each limit with the real error a public endpoint returns, and one block where eth_getLogs and the same node's receipt call disagree. Every figure is from a request you can run, captured on 2026-06-30.
1. What eth_getLogs is, and why you reach for it
eth_getLogs returns the event logs in a block range that match a filter on contract address and indexed topics. It is the one log-reading method every EVM node and provider supports, it needs no special configuration, and it is free on most public endpoints. So it is where almost every team starts when it needs token transfers, swaps, or any contract event. A typical call asks for one contract's logs over a range:
That asks an Ethereum endpoint for USDC Transfer events across a small block range, and it returns the matching logs. The trouble starts as the range grows, and as the data gets older.
2. The caps: no portable page size
Every endpoint bounds eth_getLogs to limit its own work, and a query past the bound is rejected, not truncated. Ask one public endpoint for all logs across a 2,000-block range and it answers with a result cap rather than data:
The suggested range is about fifteen blocks. The hard part is that the cap is not standardized: some endpoints limit the result count, others the block span, and the numbers are not close. The same all-logs query returned a different limit from each one:
- 1rpc.io50 blocks per query
- eth.merkle.io1,000 blocks per query
- rpc.mevblocker.io10,000 results per query
- ethereum-rpc.publicnode.comrecent blocks only (see below)
A page size that is safe on one endpoint overflows on another by 20x, and the right size also depends on the data, since a busy contract emits more logs per block. So pulling a long history means a loop: request a window, catch the cap error, halve the range, retry, and stitch the pieces back together. The work is not in the query, it is in the paging around it.
3. History sits behind an archive node
The range cap is the limit you hit on recent data. Reach back further and a second one appears: serving logs for old blocks requires an archive node, the most demanding configuration to run, and public endpoints commonly gate it. The same query that worked on recent blocks returns this once the range is no longer near the head:
To backfill a contract's full event history this way, you either run your own archive node with logs indexed, or pay for an endpoint that allows deep historical eth_getLogs, and then you are back to paging it a few hundred or a few thousand blocks at a time across years of chain. This is the reason teams reach for an indexed source rather than the raw method: not the query, the scale of running it.
4. The logs the bloom index hides
The subtler problem is that a successful eth_getLogs can come back incomplete, with no error to warn you, and which logs it returns can depend on the node. The method is usually served from a log index the node builds from each block's logsBloom, so a log the bloom does not commit to enters that index only if the node puts it there deliberately, and not every node does. Polygon state-sync logs are the case to watch. Polygon's docs say a standard node returns them through eth_getLogs; the archive node queried here does not. Ask it for every log in Polygon block 74,614,768:
848 logs. Now ask the same node for the receipt of the block's state-sync transaction, 0x167f…3ec7:
The receipt carries 8 logs that eth_getLogs did not return, from the same node, for the same block. They are not in the bloom, so they are not in this node's log index, so the log query skips them while the receipt call reads them directly. The full block has 856 logs; this endpoint's eth_getLogs reports 848. The receipt always carries them; eth_getLogs is the path you cannot assume.
- eth_getLogs (whole block)848
- eth_getLogs (address = state-receiver 0x…1001)0
- eth_getTransactionReceipt (the state-sync tx)8
- SQD Portal (whole block)856
A pipeline built on eth_getLogs alone would carry 848 and never know about the 8, because nothing in the response signals the omission. Why these logs are outside the bloom in the first place, and why no Merkle proof catches it either, is the subject of the companion guide on the onchain data block proofs leave out.
5. eth_getLogs with SQD
An indexed data layer removes the three frictions at once: it has already extracted the logs, so there is no per-query cap, no archive node, and no bloom blind spot. The same Polygon block, asked of SQD's Portal, returns all 856 logs, the state-sync ones included, from a single keyless request:
The same request shape runs across an arbitrary block range rather than 50 or 1,000 at a time, over every EVM network from genesis, with no archive endpoint to provision. For a pipeline you would stream with the Squid and Pipes SDKs, which pull the same decoded logs into your own store. For how this fits a full data pipeline, see RPC vs indexed data and the analytics solution.
Frequently asked questions
Why does eth_getLogs return an error on a wide block range?
What is the maximum block range for eth_getLogs?
How do I paginate eth_getLogs?
Can eth_getLogs miss logs that exist?
What is the alternative to eth_getLogs for large queries?
Related guides
Done paginating eth_getLogs?
Stream logs across any range, from genesis, with no caps and no archive node, from Portal.