- Category
- Operations
- Reading time
- 9 minutes
- Published
Block receipts vs transaction receipts for EVM indexers
An indexer that needs the outcome of every transaction in a block has two obvious retrieval paths: ask for all receipts in one call, or fetch each receipt by transaction hash. A third path, eth_getLogs, is often better when the product needs only events from selected contracts. These calls return different scopes of data, and fewer requests alone does not prove a faster or more reliable ingestion job.
The useful decision is whether you need complete execution outcomes, one known transaction, or filtered events. Once you choose, you still need to prove that the returned data belongs to the block you intended and that a reorganization cannot silently corrupt the index. This guide covers that process from method selection through a safe checkpoint.
Which method should an indexer use?
Use eth_getBlockReceipts when the job requires every transaction receipt in a known block and the target endpoint supports that method and block depth. Use eth_getTransactionReceipt for one known transaction hash, or as a controlled fallback after obtaining the block's transaction hashes. Use eth_getLogs when the job only needs events selected by address or topics; fetching every receipt just to discard most logs usually adds avoidable data transfer.
The Ethereum Execution API defines the block method as returning a receipt array by number, tag, or hash. Its transaction receipt method takes one transaction hash and returns one receipt or null. The log method accepts a filter, including a single blockHash query. Support, retention, response limits, and error behavior still need testing on the exact EVM network and endpoint you will use.
What information does a receipt provide?
A transaction receipt ties an included transaction to a block and records its execution outcome. Useful fields include transactionHash, transactionIndex, blockHash, blockNumber, status for post-Byzantium Ethereum transactions, gas used, contract creation address, and emitted logs. A receipt is not a full transaction, a trace, or a snapshot of contract state. If your product needs the sender's calldata or the sequence of internal calls, plan a separate data request.
Receipt logs are the raw material for many event indexers. A filtered eth_getLogs response can be enough for a token-transfer view, while an accounting indexer that records success, failure, gas, and every emitted event may need whole-block receipts. Decide from the columns your database must actually store, then test those fields on representative transaction types. The receipt schema is the reference for field presence; do not assume every historical receipt has the same optional fields.
How do you fetch a complete block safely?
First resolve and store the block number, hash, parent hash, and transaction hashes with eth_getBlockByNumber or eth_getBlockByHash. Then call eth_getBlockReceipts with the block hash if that parameter is accepted. A hash fixes the target block while latest can move between calls and a number can later name another canonical block.
Validate before writing: the receipt array length should equal the block's transaction count; each transaction hash should occur exactly once; each receipt's blockHash and blockNumber should match the chosen block; and its transactionIndex should map to the expected transaction hash. An empty array is correct for a block with zero transactions, but it is not complete for a block that contains transactions. Treat null, a JSON-RPC error, a missing receipt, a duplicate, or a mismatched block identity as an unresolved block. Do not advance the durable cursor. The block API can return transaction hashes without full transaction objects, so this check does not require downloading every transaction body.
When is eth_getLogs the better choice?
If you need events from a few contracts or topics, request those logs directly. The filter can target one block hash or an inclusive block range. That avoids receiving receipts for unrelated transactions. It does not return a receipt's execution status, gas fields, or a complete list of transactions that emitted no matching event.
Choose a single-block blockHash filter when verifying one block or repairing a reorg. For a long historical range, page bounded fromBlock and toBlock requests, verify boundaries, and adapt range size to actual response behavior. If the application later needs status or fees for a matched transaction, fetch its receipt by hash as an additional step. The existing log backfill guide covers range paging and checkpoint recovery in detail.
Do old receipts require an archive node?
Receipt history and historical world state are different requirements. A request for an old receipt does not inherently read an old balance or storage slot, but it still fails if the endpoint no longer retains that receipt or cannot serve the requested range. The current Execution API includes a pruned-history error for affected calls.
Test the oldest block your product needs, a busy block, a sparse block, and the exact method and block-reference form used by the job. Check both result completeness and errors. Avoid treating the label “archive” as proof of receipt retention or throughput; the archive decision guide explains why state, receipts, logs, and traces need separate qualification.
How do you keep receipts correct across a reorganization?
Store every indexed row with its block hash and transaction identity. Before committing block N, check that its parentHash equals the hash committed for N−1. If the chain has changed, find the common ancestor, remove data tied to orphaned hashes, and replay the new canonical blocks. Use a confirmation boundary that matches the product's tolerance for temporary data, and keep rollback logic even if the job normally waits behind that boundary.
Commit the receipts, derived rows, and cursor in one database transaction. On a crash, either all of the block is committed or none of it is. Make replay idempotent with keys that include chain and transaction identity, plus block identity where fork history must be retained. A successful RPC call is only an input to the checkpoint; validation and the database commit make the indexer's progress durable.
How should you compare the two receipt paths?
Benchmark the finished block, not an isolated RPC call. Replay the same recent and historical block sample through each supported path. Include empty and dense blocks, transactions with many logs, and blocks near your required retention boundary. Measure completed and verified blocks per minute, response bytes, timeouts, throttling, retries, memory use, and the oldest uncommitted block. Include parsing and database commit time.
One block-receipts call can reduce request count, yet its payload may be large and a failed response may require repeating the whole block. Per-transaction calls permit smaller retries but add scheduling and response-matching work. Filtered logs may transfer far less data when only a small event subset matters. Choose the path that completes your actual workload within its correctness and cost constraints; do not infer a universal winner from request count alone.
Receipt indexing checklist
- Define whether the output needs all transaction outcomes, selected receipts, or filtered events.
- Verify the exact method, block-hash parameter, history depth, and limits on the production endpoint.
- Resolve the block hash, parent hash, and transaction-hash list before requesting whole-block receipts.
- Check receipt count, unique transaction hashes, transaction indexes, and block identity.
- Bound fallback concurrency, batch size, response bytes, and retry deadline.
- Treat
null, errors, and incomplete arrays as unresolved work, never as an empty block. - Commit derived rows and the block cursor atomically.
- Preserve block identity so a reorganization can be rolled back and replayed.
- Benchmark verified blocks per minute on representative current and historical data.
The practical rule: a block is complete only when every expected receipt has been checked against that block and its checkpoint has been committed.