You fetch smart contract events from Ethereum with the eth_getLogs JSON-RPC method for historical data, or an eth_subscribe WebSocket subscription for events as they happen. Both read from the same underlying data structure — the event logs a contract writes when it fires an event — just on different timelines. This guide walks through what that data actually is, why it exists, who queries it, and how to pull it correctly once your queries get bigger than a single test transaction.
What Are Smart Contract Events, and Why Do Logs Exist?

A smart contract event is a message a contract emits during execution to record that something happened, without writing that data into the contract’s own storage. When a Solidity contract runs emit Transfer(from, to, amount), the Ethereum Virtual Machine writes a log entry attached to that transaction’s receipt rather than saving it as contract state.
That distinction is deliberate, and it comes down to gas. Writing 32 bytes to contract storage costs 20,000 gas, while a log costs a flat 375 gas plus 375 gas per topic plus 8 gas per byte of data, according to ethos.dev’s breakdown of Ethereum’s gas schedule. Logs were built as a cheap, write-only record — a contract can emit one but can’t read it back, which is why anything the contract itself needs later still has to live in storage.
Each log carries two kinds of parameters: indexed and non-indexed. Indexed parameters go into the log’s topics array and can be filtered directly in a query; non-indexed parameters get packed into the data field and have to be decoded afterward. A Solidity event can mark up to three parameters as indexed, and the event’s own signature hash fills the first topic slot automatically, so a single log tops out at four topics total.
Why Do Developers Need to Fetch Contract Events?
The practical problem is that logs are the only efficient way to reconstruct what a contract has done over time. Re-running every historical transaction against a contract to rebuild its history would mean replaying the entire chain’s execution, which no application can afford to do on every page load.
Events solve this by giving you a queryable trail without touching contract state at all. A DEX doesn’t store a list of every swap that ever happened — it emits a Swap event on each trade and lets anyone who cares reconstruct that history from the logs. That’s also why an ERC-20 token contract typically doesn’t store a full transfer history on-chain; the Transfer event is the record, and it’s up to wallets, explorers, and indexers to collect it.
This matters for anything that needs to show activity rather than just current state. Current state answers “what is my balance right now”; event logs answer “how did it get there.”
Who Actually Queries Event Data?
A handful of distinct groups pull event logs, each for a different reason:
- Wallets and portfolio trackers watch
Transferevents to detect incoming funds and rebuild transaction history for an address. - Block explorers decode logs to show a human-readable activity feed under every contract and transaction.
- DeFi dashboards and analytics platforms aggregate
Swap,Mint, andBurnevents to calculate volume, liquidity, and price history. - Indexers and subgraphs ingest logs continuously to power the fast, filtered queries that a raw node can’t serve efficiently.
- Security teams monitor specific events — a large withdrawal, an ownership transfer, a pause function — to catch exploits or unusual activity close to real time.
- Exchanges and payment processors watch deposit-address
Transferevents to credit user accounts without polling every block manually.
The common thread is that none of these groups want to run their own execution against every past block. They want a filtered, decoded slice of history, and eth_getLogs is what makes that possible without a full archive replay.
How to Fetch Events With eth_getLogs
eth_getLogs takes a filter object and returns every log matching it. Here’s the sequence for a typical query against an ERC-20 Transfer event.
- Compute the event signature hash. Take
keccak256("Transfer(address,address,uint256)"), which produces0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. This becomestopics[0]in your filter — it’s how the node knows which event you’re asking for. - Set the block range. Provide
fromBlockandtoBlock, either as hex block numbers or tags like"latest". A tighter range returns faster and is less likely to hit a provider’s size limit. - Specify the contract address. This narrows the search to logs from one contract instead of scanning every log in the range.
- Add topic filters for indexed parameters, if you want to filter further — for example,
topics[1]set to a specific sender address, left-padded to 32 bytes. - Send the request and decode the response. Non-indexed values arrive in the
datafield as packed hex and need an ABI decoder —viem‘sdecodeEventLogor ethers.js’sInterface.parseLogboth handle this.
A minimal request looks like this:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getLogs",
"params": [{
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"fromBlock": "0x112A880",
"toBlock": "0x112A8F0",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}]
}
Each log in the response includes blockNumber, transactionHash, address, topics, and data — enough to trace exactly which transaction produced it, even before you decode the payload.
Real-Time Events With WebSocket Subscriptions
Polling eth_getLogs on a timer works, but it wastes requests waiting for something that might not have happened yet. For anything that needs to react as events land — a bot watching for a specific Swap, a dashboard updating live — a WebSocket subscription is the better fit.
The eth_subscribe method with a "logs" parameter pushes matching log entries to your client the moment a node processes them, over one persistent connection instead of repeated HTTP calls:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribe",
"params": ["logs", {
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]
}]
}
NOWNodes’ WebSocket API supports this subscription pattern on Ethereum, so an application can stream event data without maintaining an always-on connection to its own client. The trade-off against eth_getLogs is straightforward: subscriptions are for what’s happening now, while eth_getLogs is what you reach for to backfill everything that happened before your listener was running.
What Are the Real Limits of eth_getLogs?
This is where most eth_getLogs implementations run into trouble, because providers cap queries differently and none of it is standardized. Elan Halpern, who works on developer experience at Alchemy, put the core tension plainly: leave the block range too open and “we can risk trying to query millions of logs” against a single request, which is exactly what providers are built to prevent.
The caps themselves vary widely. Alchemy allows a 2,000-block range with no response-size limit, or any block range capped at 10,000 logs in the response — whichever fits your query better, according to Halpern’s deep dive into the method. QuickNode’s default cap sits at 10,000 blocks per request, as its support documentation explains, while a survey by indexing provider SQD found free public endpoints ranging from 50 blocks per query up to 1,000, with no consistency between them.
| Provider / endpoint type | Typical range or size limit |
|---|---|
| Alchemy | 2,000 blocks (unlimited response) or unlimited range (10,000-log response cap) |
| QuickNode | 10,000 blocks by default |
| Public free endpoints (varies) | As low as 50–1,000 blocks per query |
Three practical consequences follow from this:
- Pagination is mandatory for wide historical queries. Split a multi-year range into chunks that fit under whichever cap your endpoint enforces, and loop through them.
- Archive access matters for older data. A pruned node only keeps recent state, so queries reaching back beyond its retention window need an archive-enabled endpoint.
- Bloom filters aren’t perfectly complete. SQD documented a case on Polygon where
eth_getLogsreturned 848 logs for a block whose transaction receipts actually contained 856 — eight state-sync logs missing from the bloom index, with no error to flag the gap. It’s a rare edge case, but a reminder thateth_getLogsresults are only as reliable as the index behind them.
When Should You Use an Indexer Instead of Direct Queries?
eth_getLogs is the right tool when you know the contract, the event, and roughly the block range you need. It gets impractical once you need to join event data across many contracts, run complex filters, or serve fast queries to end users at scale — that’s the gap indexing services like The Graph are built to fill.
| Approach | Best for | Trade-off |
|---|---|---|
Direct eth_getLogs | Known contract, known event, moderate range | You manage pagination and rate limits yourself |
| WebSocket subscription | Real-time monitoring of new events | No historical backfill on its own |
| Subgraph / indexer | Cross-contract queries, high query volume, GraphQL access | Added infrastructure and indexing lag to account for |
Most production applications end up using more than one of these together: a subscription for live updates, eth_getLogs for backfilling history on first load, and an indexer once query complexity outgrows what direct log queries can serve efficiently.
How to Decode Event Logs Correctly

Getting a raw log back is only half the job — the data field is packed hex that means nothing without the contract’s ABI. Both major libraries handle this: viem‘s decodeEventLog and ethers.js’s Interface.parseLog take the ABI fragment for the event and return named, typed values instead of a hex blob.
One nuance catches developers off guard: indexing a dynamic type — a string, bytes, or array — doesn’t put the actual value in topics. It stores keccak256 of that value instead, because a topic slot is a fixed 32 bytes and a dynamic type has no fixed size. You can filter by that hash to check for an exact match, but you can’t recover the original string from the topic itself; it has to come from elsewhere in the transaction, such as an unindexed copy of the same value in data.
Getting an accurate picture of a contract’s activity also means checking what actually goes into an Ethereum transaction in the first place — logs are a byproduct of execution, and understanding the transaction that produced them makes debugging a missing or unexpected event much faster.
Fetching Events at Scale With NOWNodes
Once event queries move past occasional lookups into continuous monitoring, the constraint usually isn’t the query logic — it’s how much request volume and uptime the underlying endpoint can sustain. NOWNodes’ Ethereum endpoint exposes eth_getLogs alongside archive access and WebSocket subscriptions on the same network, so a team can pair historical backfills with live monitoring without switching providers between the two.
For workloads that run high query volumes around the clock — a security monitor scanning thousands of contracts, or an indexer rebuilding history for a large protocol — a dedicated endpoint removes the shared-quota ceiling that a standard plan enforces, since throughput is then bound by allocated hardware rather than a fixed request count. That’s a different problem than the query mechanics covered above, but it’s the one that shows up first once event-fetching moves from a script into production infrastructure.
Conclusion
Fetching smart contract events comes down to picking the right tool for the timeframe: eth_getLogs for anything already on-chain, a WebSocket subscription for anything still happening. The mechanics — topic hashes, block ranges, ABI decoding — are the same regardless of provider, but the caps and edge cases around them aren’t, so it’s worth checking a provider’s specific limits before building a pagination strategy around assumed defaults. Get that part right, and event logs turn into exactly what they were designed to be: a cheap, complete record of what a contract has done, without ever touching its storage.
FAQ
Can eth_getLogs return logs from internal contract calls?
Yes. Logs are attached to the transaction, not the specific call frame, so eth_getLogs returns events emitted by any contract touched during that transaction’s execution — not just the one the transaction was sent to directly.
How many topics can a single log have?
Four. Topic 0 is always the keccak256 hash of the event signature, and Solidity allows up to three additional parameters to be marked indexed, filling topics 1 through 3.
Do I need an archive node to query old events?
It depends on the range and the endpoint’s pruning policy. Many providers keep enough recent history for typical queries, but reaching back further — months or years — generally requires an archive-enabled endpoint rather than a standard pruned one.
What happens if a query exceeds a provider’s log limit?
The request fails with an error naming the limit, rather than returning partial results. The fix is splitting the query into smaller block ranges and looping through them, not retrying the same range.
Can I filter on non-indexed event parameters directly?
No. eth_getLogs can only filter using the topics array, which covers the event signature and indexed parameters. Anything stored only in data has to be fetched broadly and filtered client-side after decoding.



