Events and logs: eth_getLogs, bloom filters and public RPC limits
How smart-contract events work, how to read them via eth_getLogs, why public nodes cut the block range and how to build deal history without your own database.
In this article
A smart contract has two ways to tell the world what happened: change state (storage) and write an event. State shows "how it is now". Events tell "how it got there". For interfaces showing deal history, events are the main data source.
What an event is
An event is an entry in a transaction's journal. A contract does emit DisputeOpened(dealId, by, at), and a log appears in the transaction receipt: the contract address, a set of topics and data. The log's structure was covered in the article on ABI: topic0 is the event identifier, indexed arguments are the following topics, the rest is data.
Important properties of events:
- Cheap. Writing an event is many times cheaper than writing to storage.
- Unavailable to contracts. A contract cannot read its own or others' events - they are only for the outside world.
- Permanent. Once written, a log stays in the chain's history.
- Tied to the transaction. If a transaction is rolled back, so are its events.
Reading events: eth_getLogs
An interface gets events from a node with an eth_getLogs request and a filter:
{
"address": "0x6462DDDc0a3D893A7C17f94410e2d27162a390aD",
"fromBlock": "0x…",
"toBlock": "0x…",
"topics": [
["0x2241d0fd…", "0xd716fe62…"],
"0x0000…0007"
]
}Here topics[0] is a list of events of interest (several variants allowed), topics[1] is the value of the first indexed parameter, for example the deal number. Filtering by address and topics is the reason parameters are indexed: this is how a node finds what is needed quickly.
The reply is a list of logs with block number, transaction hash, topics and data. The interface then decodes them by the ABI and builds the feed: "created", "accepted", "dispute opened", "settled".
How a node searches fast: bloom filters
Scanning all blocks in a row would be painfully slow. Each block header carries a bloom filter - a compact structure that quickly answers "this block definitely has no events of this contract with this topic" or "there may be". The node discards empty blocks and checks only the likely ones. The filter allows false positives but never misses a real event.
A practical problem: public node limits
Here begins what textbooks rarely mention. Public RPC nodes are free, so they protect themselves from expensive requests. A log request over millions of blocks is exactly that. From our experience connecting four networks:
- Some nodes limit the block range per request - from 2,000 to 50,000, depending on network and provider.
- Some do not serve logs at all (for example one of the public BNB Chain nodes).
- The trickiest: some nodes, given a range that is too wide, answer not with an error but with an empty list. The interface sees "no events" and shows an empty history though deals exist.
- Public nodes often do not keep old state: you cannot ask "how it was 100,000 blocks ago".
Several rules for the developer follow.
Rule 1. Do not trust an empty reply
An empty list for a wide range can mean "no events" or "the node refused". Check on a known event when setting up a new node, and in code do not decide "no events" from one request.
Rule 2. Move in windows and adapt
If you need recent events, do not request the whole history. Go from fresh blocks to old ones in windows, and if the node returns a limit error, halve the window. Stop when you have found enough data or reached the contract's deployment block. A sketch of such a walk:
// from fresh blocks to old ones, the window shrinks on error
let to = latest;
let span = 50_000n;
while (to > deployBlock && found.length < needed && budget-- > 0) {
const from = to - span < deployBlock ? deployBlock : to - span;
try {
found.push(...(await getLogs({ fromBlock: from, toBlock: to })));
to = from - 1n;
} catch {
span /= 2n; // the node refused - narrow the window
if (span < 1000n) break;
}
}Rule 3. Know the deployment block
There is no point scanning blocks before the contract existed. The deployment block is a configuration parameter that bounds the search.
Rule 4. Do not overbuild: a counter is often better than logs
For a "user's deals" list it is convenient for the contract to keep a dealCount counter. While deals are few, they can be enumerated by number with batched state reads (Multicall) - no logs needed at all. Logs come in when there are too many deals to enumerate. We use exactly this hybrid scheme: it works even on nodes that do not serve logs.
When logs are not enough: indexers
For large volumes and complex queries ("all of a user's deals over a year, sorted by amount") you need an indexer - a separate service that reads all events once into its own database and answers queries fast. Examples: The Graph, Ponder, home-grown solutions. It is not trusted blindly: its data is a copy of the chain, the blockchain remains the source of truth, and important actions should be re-checked by reading the contract directly.
Chain reorganisations
Recent blocks can be rewritten (a reorg): the network changed its mind about the latest blocks, and your transaction ended up in another block or in none. So interfaces do not treat an event as final until enough confirmations have passed, and indexers know how to "roll back" the latest blocks.
What to read next
Now that it is clear how data gets from the blockchain into the interface, it is time to write a contract of your own: ERC-20 from scratch. Next, how signatures work in contracts: ECDSA and EIP-712, and then the escrow state machine in Solidity.
Keep reading
- 2 · Practice
How to read Etherscan: checking a contract, a transaction and events
A practical guide to block explorers (Etherscan, Basescan, Polygonscan, BscScan) - how to check an address, a transaction, a contract, tokens and approvals.
4 min read - 3 · Under the hood
ABI: how a function call turns into bytes - selectors, calldata and events
How smart-contract calls are encoded - the function selector, argument alignment, dynamic types, custom errors and event topics, with real examples.
5 min read
Mentioned in
- Glossary - blockchain, wallets, smart contracts and escrow from A to Z
- The deal lifecycle - from offer to payout, step by step
- How to read Etherscan: checking a contract, a transaction and events
- How to verify a smart contract before using it - a step-by-step guide
- A tour of the Solidity language - types, functions, errors and events by example
- ABI: how a function call turns into bytes - selectors, calldata and events
- ERC-20 from scratch - writing a token in Solidity and analysing every line
- Writing escrow in Solidity: the state machine and a real contract walkthrough
- Storage layout and variable packing in Solidity
- Gas optimization in Solidity - 12 techniques and when they apply
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

