How we rebuilt historical logs for speed
Author: Alchemy

Ask any team building an indexer, a DEX aggregator, or an onchain monitoring system where their app slows down, and the answer is almost always the same: large-range eth_getLogs.
It is the call that scans thousands or hundreds of thousands of blocks to answer one question. Which transfers hit this contract last month? Which pools changed state across this range? What events fired for this address since launch? These reads are the backbone of real applications, and they are also the reads most likely to stall. So we rebuilt the engine that serves them, and made the method over 2x faster at p99 in every region. Here is how.
What is eth_getLogs and why does it matter?
eth_getLogs is the JSON-RPC method that returns the events smart contracts emit as they run: a token transfer, a swap, an approval, or any custom event a contract defines. You filter by contract address, by event signature or indexed topic values (like Transfer or Approval), and by a block range with a fromBlock and a toBlock. In plain terms: tell it what to look for and how far back to look, and it returns every matching event.
This is the foundation of event-driven indexing on Ethereum. Without it, you would scan every transaction by hand to find the ones you care about. With it, you build activity and transfer feeds, approval monitoring, and any real-time or historical event indexer. It is one of the most heavily used, operationally critical methods we serve. If you are new to logs and topics, our deep dive into eth_getLogs walks through the request and response structure with a worked example.
The rest of the method shapes the engineering problem. Many providers cap how many blocks you can query at once. Indexers have to handle chain reorgs by waiting for confirmations. Topic filters only match indexed fields, so non-indexed arguments get decoded from the raw data. And calls near the chain head are latency-sensitive, where routing to the wrong node type can cause errors or empty responses.
The part that shapes latency most is range size. Asking for one block is instant. For an indexer replaying history, or a monitor watching a contract over weeks, the ranges get wide fast, and the cost of the query climbs with them. That is where latency stops being a rounding error and starts to gate how fast a customer can build.
Everyone offers the method. Fewer people talk about the engine underneath it, because rebuilding that engine is expensive and most providers have not done it.
Why does p99 latency matter more than average latency?
The number that hurts is not the average. It is p99: sort every request from fastest to slowest, and p99 is the slowest one in a hundred. Averages hide it. You can serve 99 requests in 60 milliseconds and still watch the hundredth take a full second.
That slowest request is the one that makes a dashboard hang or an indexer fall behind the chain, and it is the one your user is staring at. Average latency can look fine while the tail quietly breaks the workload. For teams running indexers and monitors, the tail is the workload.
How did we optimize eth_getLogs?
We rebuilt the storage and query engine that serves the method, rather than tuning the one we had.
There are a handful of ways to make a call like this faster. You can tune query parameters, add a cache, cap the ranges you accept, or throw more of the same hardware at it. Most of those buy a little headroom without changing the fundamentals.
We went deeper. Instead of relying on a general-purpose managed database that treats historical log reads like any other workload, we run a storage and query layer tuned specifically for the getLogs access pattern: fast filtered scans over very large datasets, on dedicated infrastructure we operate ourselves.
We did not tune a knob or bolt a cache onto the old system. We invested at the method level, in the engine underneath one of the most critical calls our customers make. It is expensive, per-method infrastructure work, and it is why the improvement shows up in the tail, not just the average.
What are the results?
The headline is simple: over 2x faster at p99, in every region.
If you run an indexer or a monitor, p99 is the latency that breaks your workload. A better average is nice. A better tail is what keeps a dashboard responsive and an indexer caught up to the chain.
Teams running this at serious scale are already seeing it. One of the largest crypto exchanges benchmarked their historical log reads on our engine against running their own nodes, and measured a large p99 improvement on their own workload. That is proof in production, on a customer's own instrumentation, not just on our dashboards.
What does this mean for you?
If your application leans on historical log reads for indexing, aggregating, or monitoring, you get faster reads and a dramatically better p99 tail, in every region, on infrastructure we rebuilt specifically for this method. A few things worth knowing as you put it to work:
- On pay-as-you-go and enterprise plans,
eth_getLogssupports unlimited block ranges in a single query on Ethereum, Base, Optimism, Arbitrum, Polygon, Robinhood and other major chains, so replaying history does not require hand-rolled pagination. - For logs at the chain head, WebSocket subscriptions push new logs to you as they arrive, so you reserve
eth_getLogsfor the historical reads it is built for. - Nothing about your integration changes. It is the same method, the same request shape, served by a faster engine.
The point of investing at the method level is that the reads that define your workload are the ones we tuned the engine for. Create a free Alchemy account and point your indexer at us to see the difference on your own instrumentation.
Frequently asked questions
What does eth_getLogs return?
An array of event logs matching your filter: contract address, event signature and indexed topic values, and a block range. Each log includes the emitting address, topics, data payload, and the block and transaction it came from. See the API reference for the full request and response spec.
Why is p99 latency more important than average latency for indexers?
Because an indexer processes requests in sequence or in batches, the slowest requests set the pace. A low average with a long tail means the indexer regularly stalls on the slow one percent, falls behind the chain head, and serves stale data. p99 measures exactly those requests.
What block ranges can I query in a single eth_getLogs call on Alchemy?
On the free tier, 10 blocks per query. On pay-as-you-go and enterprise plans, block ranges are unlimited on Ethereum, Base, Optimism, Arbitrum, Polygon, Worldchain, BNB, and several other chains, with responses capped at 150MB. The full per-chain table is in the eth_getLogs reference.
How do I keep large historical queries fast?
Filter as tightly as you can: specify the contract address and the event topics you need, not just a block range. For very wide ranges on chains without unlimited-range support, split the query into smaller chunks. And for new logs going forward, use WebSocket subscriptions instead of polling.
Alchemy Newsletter
Be the first to know about releases
Sign up for our newsletter
Get the latest product updates and resources from Alchemy
By entering your email address, you agree to receive our marketing communications and product updates. You acknowledge that Alchemy processes the information we receive in accordance with our Privacy Notice. You can unsubscribe anytime.
Related articles

Introducing historical Solana token balances
getTokenAccountsByOwnerAtSlot returns everything a wallet held at any past point in time, with exact balances, in a single call.

How we fine tune our RPC for speed and reliability
We moved compression for large RPC responses to a better layer in our serving path, cutting P95 latency on heavy calls and reducing connection churn.

How Alchemy's node infrastructure keeps DeFi fast at scale
DeFi teams depend on latency, reliability, and scale. See how 0x, Solflare, and Usual run high-pressure workloads on Alchemy's infrastructure.