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.

Updated 2026-06-30 · By the SQD team

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:

your terminal
curl https://rpc.mevblocker.io -H 'content-type: application/json' \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "eth_getLogs",
"params": [{
"fromBlock": "0x1840b3a",
"toBlock": "0x1840b3c",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}]
}'

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:

rpc.mevblocker.io
{
"error": {
"code": -32005,
"message": "query returned more than 10000 results. Try with this block range [0x184036C, 0x184037B].",
"data": { "from": "0x184036C", "limit": 10000, "to": "0x184037B" }
}
}

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:

Public endpointeth_getLogs limit
  • 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)
What public endpoints returned for a wide eth_getLogs query (2026-06-30)

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:

ethereum-rpc.publicnode.com
{
"error": {
"code": -32602,
"message": "Archive requests require a personal token. Get one at: https://www.allnodes.com/publicnode"
}
}

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:

polygon.drpc.org
curl https://polygon.drpc.org -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs",
"params":[{"fromBlock":"0x47287f0","toBlock":"0x47287f0"}]}'

848 logs. Now ask the same node for the receipt of the block's state-sync transaction, 0x167f…3ec7:

polygon.drpc.org
curl https://polygon.drpc.org -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt",
"params":["0x167f905bee2860765a0f4996085e826f9a9aa60ec4fff225b454026ef1783ec7"]}'

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.

MethodLogs
  • 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
Logs in Polygon block 74,614,768, polygon.drpc.org (queried 2026-06-30)

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:

your terminal
curl https://portal.sqd.dev/datasets/polygon-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 74614768,
"toBlock": 74614768,
"logs": [{}],
"fields": { "log": { "logIndex": true, "address": true, "topics": true } }
}'

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?
Public endpoints cap the method to bound their own work, and they do it in two different ways: by block range, or by result count. A query that exceeds either is rejected rather than truncated, so you get an error instead of a partial answer. The caps are not standardized, so the same request that works on one endpoint fails on another, and the only general fix is to split the range and retry.
What is the maximum block range for eth_getLogs?
There is no protocol maximum; each provider sets its own. In a check across public endpoints in mid-2026 the limits ranged from 50 blocks to 1,000 blocks per query, with others capping at 10,000 matched results regardless of range and some serving recent blocks only. Because there is no portable page size, robust code discovers the limit from the error and halves the range until the query fits.
How do I paginate eth_getLogs?
Split the block range into windows small enough to stay under the endpoint's cap, request each window, and concatenate. Some endpoints return the suggested next range in the error payload. The window size is workload-dependent, since a busy contract produces more logs per block, so a fixed page size either wastes requests or overflows; adaptive splitting on the error is the reliable pattern. An indexed stream avoids the loop by not imposing the cap in the first place.
Can eth_getLogs miss logs that exist?
It can return an incomplete set without erroring, and the result depends on the node. eth_getLogs is usually served from a log index built from the block's logsBloom, so logs the bloom omits, such as Polygon state-sync logs, are in that index only if the node adds them deliberately. On the archive endpoint tested for this guide eth_getLogs left them out while eth_getTransactionReceipt returned them, so the receipt is the dependable path. The companion guide on what block proofs leave out covers the mechanism.
What is the alternative to eth_getLogs for large queries?
An indexed data service that has already extracted and stored logs serves them without the per-query caps and without an archive node. SQD's Portal streams logs (and transactions, traces, and state diffs) across an arbitrary range in one request, returns the bloom-omitted logs too, and runs from genesis, so the pagination loop and the archive requirement both go away.

Done paginating eth_getLogs?

Stream logs across any range, from genesis, with no caps and no archive node, from Portal.