eth_getBlockReceipts(blockParameter) returns an array of every transaction receipt in a block in one JSON-RPC round trip, using the same block parameter forms as eth_getBlockByNumber (number, tag, or 32-byte hash). A receipt is the post-execution summary of a transaction — status, cumulativeGasUsed, gasUsed, effectiveGasPrice, logsBloom, logs, contractAddress, and type — and the block's receiptsRoot commits to all of them. Looping eth_getTransactionReceipt once per transaction multiplies round trips and rate-limit pressure by the transaction count, so bulk retrieval is the better fit for indexers and per-block analytics passes. Method availability is client- and provider-dependent, so detect support at runtime and fall back to per-transaction calls when the bulk method returns method-not-found.
What a Receipt Is and Why the Block Commits to All of Them
A transaction receipt is the post-execution summary of a transaction. It tells you whether the transaction succeeded or reverted, how much gas it consumed, what it paid per unit of gas, what logs it emitted, and whether it deployed a contract. The Ethereum execution-apis JSON-RPC specification defines the receipt object fields, and the ethereum.org JSON-RPC API listing documents the same shape for public consumption.
The fields that matter most for analytics are status (1 for success, 0 for revert), cumulativeGasUsed (gas used by all transactions up to and including this one in the block), gasUsed (gas used by this transaction alone), effectiveGasPrice (the actual per-gas price paid after EIP-1559), logsBloom (a probabilistic filter over the block's logs), logs (the emitted event records), contractAddress (set when the transaction created a contract), and type (the transaction type).
Each block header contains a receiptsRoot, a Merkle-Patricia root that commits to every receipt in the block. That commitment is why receipts are the canonical proof of a block's execution outcomes: if you have the receipts, you can verify what the block actually did, not just what transactions it contained. For a broader view of how these calls fit into a node setup, see Choosing an Ethereum RPC node.
- status: 1 = success, 0 = revert; the first field most indexers branch on.
- gasUsed vs cumulativeGasUsed: per-transaction vs running total within the block.
- effectiveGasPrice: the post-EIP-1559 price actually paid, used to compute feePaid.
- logs and logsBloom: emitted events and the block-level bloom filter over them.
- contractAddress: non-null only when the transaction deployed a contract.
- type: transaction type, useful when mixing legacy and typed transactions.
eth_getBlockReceipts: One Call, Every Receipt
eth_getBlockReceipts(blockParameter) returns an array of all receipts for the block identified by blockParameter. The parameter accepts the same forms as eth_getBlockByNumber: a block number, a tag such as latest, safe, or finalized, or a 32-byte block hash. The response is an array ordered by transaction index, so receipt[i] corresponds to the transaction at index i in the block.
The alternative pattern is to fetch the block with eth_getBlockByNumber, read its transactions array, then call eth_getTransactionReceipt once per transaction hash. That works, but it multiplies round trips and rate-limit pressure by the transaction count. A block with 200 transactions becomes 1 + 200 calls instead of 1 + 1. For an indexer or block processor, that difference compounds across every block you process.
Method availability is client- and provider-dependent. Some older clients and some hosted endpoints did not expose eth_getBlockReceipts, and support can vary by network and by provider configuration. Treat it as a capability to detect at runtime rather than an assumption. The OnFinality Learn hub covers related method-level guides, including Filtering event logs with eth_getLogs and topics, which is the log-oriented counterpart to receipt retrieval.
- Bulk: 1 block call + 1 receipts call = 2 round trips per block.
- Per-transaction: 1 block call + N receipt calls = 1 + N round trips per block.
- Ordering: receipts follow transaction-index order; pair by index or by receipt.transactionHash.
- Do not assume logs are grouped by contract or topic; they are per-transaction and per-emission order.
When to Use Bulk Receipts vs Per-Transaction Receipts
Use eth_getBlockReceipts when you are processing a whole block and need every receipt: indexers, analytics passes, block explorers, gas-usage dashboards, and reorg-aware backfills. The bulk call gives you the full set in one round trip, which is the efficient shape for per-block work.
Use eth_getTransactionReceipt when you already have a specific transaction hash and need only that receipt — for example, confirming a user-submitted transaction, checking a single contract deployment, or polling for a receipt after sending a transaction. In that case the per-transaction call is the right tool and the bulk call would be wasteful.
A log-subscription or backfill design can still beat both for realtime streaming, because it pushes events as they occur rather than polling blocks. Bulk receipts are a pull-based pattern; if your workload is realtime and event-driven, a subscription plus a backfill for gaps is often the better architecture. The Ethereum transaction tracing with trace and debug namespaces guide covers the deeper introspection calls that sit below receipts when you need internal call traces.
- Bulk receipts: whole-block processing, analytics, explorers, reorg backfills.
- Per-transaction receipts: single-hash confirmation, user-facing status checks.
- Subscriptions/backfill: realtime event streaming where push beats pull.
- Tracing: when receipts are not enough and you need internal call detail.
A Practical Block-Processing Pattern: Fetch, Join, Emit
The pattern is straightforward: for each block, call eth_getBlockByNumber with withTransactions=true and eth_getBlockReceipts in parallel, then join by index. From the joined data you can emit per-transaction rows (status, gasUsed, effectiveGasPrice, feePaid = gasUsed * effectiveGasPrice, log count, contract creation) and per-block aggregates (total gas used, total fees, success/revert counts, log count).
Joining by index is the simplest approach, but you should also verify with receipt.transactionHash against block.transactions[i].hash. If the two disagree, you are likely looking at a reorg or a mismatched block parameter, and you should re-fetch rather than emit bad rows.
This pattern beats N receipt calls for an analytics pass because it keeps round trips constant per block regardless of transaction count. It still loses to a log-subscription/backfill design for realtime, because it polls rather than streams. For endpoint selection and health, see Monitoring RPC endpoints and node health.
- Parallelize the block and receipts calls to reduce wall-clock time.
- Join by index, then verify with receipt.transactionHash.
- Emit per-transaction rows and per-block aggregates from the joined set.
- Re-fetch on hash mismatch rather than emitting inconsistent rows.
Runnable Node.js: Fetch a Block and Its Receipts, Join, Print a Table
The script below uses the built-in fetch available in modern Node.js. It calls eth_getBlockByNumber and eth_getBlockReceipts in parallel, joins receipts to transactions by index, and prints a table of status, gas used, effective gas price, fee paid, log count, and contract creation. Replace the RPC URL with your own endpoint.
Run it with node script.js. If your endpoint does not support eth_getBlockReceipts, the script will surface the error and you can switch to the fallback pattern described in the next section.
const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(`${method}: ${json.error.message}`);
return json.result;
}
function hexToBigInt(hex) {
return hex ? BigInt(hex) : 0n;
}
async function processBlock(blockParam) {
const [block, receipts] = await Promise.all([
rpc('eth_getBlockByNumber', [blockParam, true]),
rpc('eth_getBlockReceipts', [blockParam])
]);
if (!block) throw new Error('Block not found: ' + blockParam);
if (!receipts) throw new Error('No receipts returned for ' + blockParam);
const rows = receipts.map((r, i) => {
const tx = block.transactions[i];
const gasUsed = hexToBigInt(r.gasUsed);
const effGasPrice = hexToBigInt(r.effectiveGasPrice);
const feePaid = gasUsed * effGasPrice;
return {
index: i,
hash: r.transactionHash,
matchesBlockTx: tx && tx.hash.toLowerCase() === r.transactionHash.toLowerCase(),
status: parseInt(r.status, 16),
gasUsed: gasUsed.toString(),
effectiveGasPrice: effGasPrice.toString(),
feePaidWei: feePaid.toString(),
logCount: r.logs ? r.logs.length : 0,
contractCreated: r.contractAddress || null
};
});
const totalGas = rows.reduce((a, r) => a + BigInt(r.gasUsed), 0n);
const totalFees = rows.reduce((a, r) => a + BigInt(r.feePaidWei), 0n);
const reverts = rows.filter(r => r.status === 0).length;
console.log(`Block ${block.number} txs=${rows.length} reverts=${reverts}`);
console.log(`totalGasUsed=${totalGas} totalFeesWei=${totalFees}`);
console.table(rows.map(r => ({
i: r.index,
status: r.status,
gasUsed: r.gasUsed,
effGasPrice: r.effectiveGasPrice,
feePaidWei: r.feePaidWei,
logs: r.logCount,
contract: r.contractCreated ? 'yes' : ''
})));
}
processBlock('latest').catch(err => {
console.error('Failed:', err.message);
process.exit(1);
});Detecting Support and Falling Back to Per-Transaction Receipts
Because eth_getBlockReceipts is not universally available, detect support at runtime. The cleanest approach is to attempt the bulk call and catch a method-not-found error, then fall back to per-transaction receipt calls. Cache the capability result so you do not pay the detection cost on every block.
The fallback issues eth_getTransactionReceipt once per transaction hash from the block's transactions array. That is the 1 + N pattern, and it is correct but more expensive. If your endpoint consistently lacks the bulk method, consider whether a different endpoint or provider is a better fit for your workload; RPC pricing and the API service pages describe how OnFinality structures access, and Ethereum networks lists the network endpoints available.
A robust fallback should also handle partial failures: if one per-transaction call fails, retry it with backoff rather than discarding the whole block. Record which blocks used the fallback so you can revisit them if you later switch endpoints.
- Attempt bulk first; catch method-not-found and switch to per-transaction calls.
- Cache the capability result per endpoint to avoid repeated detection.
- Retry individual per-transaction failures with backoff instead of dropping the block.
- Log fallback usage so you can audit endpoint capability over time.
Measuring Cost Against Your Own Endpoint: A Results Table to Fill
Do not trust generic performance claims; measure against your own endpoint. The comparison is simple: bulk receipts cost 1 block call + 1 receipts call per block, while per-transaction receipts cost 1 block call + N receipt calls, where N is the transaction count. The ratio of round trips is roughly (1 + N) / 2, which grows with block size.
Run the same block through both patterns and record wall-clock time, request count, and any rate-limit responses. Fill the table below with your own measurements. Provider-specific latency and rate limits are documented / varies by provider, so your numbers are the ones that matter for capacity planning.
A useful secondary measurement is bytes transferred: the bulk response contains all receipts in one payload, which may be larger than individual responses but avoids per-call overhead. Watch for provider response-size limits on very large blocks.
- Round trips (bulk): 2 per block, independent of transaction count.
- Round trips (per-tx): 1 + N per block, scaling with transaction count.
- Measure: wall-clock ms, request count, rate-limit errors, response bytes.
- Record block number and transaction count alongside each measurement for comparability.
| Block | Tx count | Pattern | Requests | Wall-clock ms | Rate-limit errors | Response bytes |
|-------|----------|---------|----------|---------------|-------------------|----------------|
| | | bulk | | | | |
| | | per-tx | | | | |
| | | bulk | | | | |
| | | per-tx | | | | |Failure Modes: Unsupported Method, Null Blocks, Reorgs, and Limits
Unsupported method: some clients and hosted endpoints return a method-not-found error for eth_getBlockReceipts. Handle it explicitly and fall back to per-transaction calls rather than crashing your pipeline.
Null for a pending or unavailable block: if you request a block that does not exist yet or is not available on your node, the call may return null. Treat null as a signal to retry later or to skip, not as an empty receipt set. A pending block may also have no receipts yet.
Receipts for a reorged-away block: if a reorg occurs, receipts you fetched for the old block may no longer correspond to the canonical chain. Verify by re-fetching the block and comparing hashes, and re-process the affected range. For networks with asynchronous execution semantics, receipt status timing can differ; see Monad transaction lifecycle and receipt status for a related discussion.
Using a block hash for an unfinalized block: a block hash identifies a specific block, but if that block is later reorged away, the hash may no longer resolve. Prefer tags like finalized for stable processing, and treat latest or safe as provisional. Provider block-range and response-size limits can also cause failures on very large blocks or wide ranges; check your provider's documented limits.
- method-not-found: fall back to per-transaction receipt calls.
- null result: block pending or unavailable; retry or skip.
- reorg: re-fetch and re-process the affected range.
- unfinalized hash: prefer finalized tags for stable processing.
- provider limits: block-range and response-size caps vary by provider.
Limitations and Tradeoffs: Receipts vs Logs, Archive Requirements
Receipts are not a substitute for eth_getLogs when you need to filter by topic or address across many blocks. eth_getLogs is designed for log filtering and can scan ranges efficiently, while receipts give you the full per-transaction picture for a single block. Use receipts for per-block execution outcomes and logs for cross-block event queries.
Old blocks may require an archive node. If your endpoint is not archive-enabled, historical receipt retrieval can fail or return errors for blocks outside the pruned range. Confirm your endpoint's archive capability before backfilling history.
Security and correctness tradeoffs: receipts are the canonical execution summary, but you should still verify the receiptsRoot when you need cryptographic assurance. For most analytics workloads, trusting the node's response is acceptable; for high-assurance systems, verify against the block header. Also be aware that logsBloom is probabilistic and not a substitute for scanning logs.
- Receipts: per-block execution outcomes; logs: cross-block event filtering.
- Archive requirement: historical receipts may need an archive-enabled endpoint.
- receiptsRoot: verify when you need cryptographic assurance.
- logsBloom: probabilistic filter, not a replacement for log scanning.
Troubleshooting Checklist for Bulk Receipt Retrieval
When eth_getBlockReceipts does not behave as expected, work through the following checks in order. Most issues fall into a small set of categories: method support, block availability, reorg timing, and provider limits.
Start by confirming the method is supported on your endpoint with a simple call against latest. If that fails with method-not-found, switch to the fallback. If it succeeds but returns null, check whether the block parameter is valid and whether the block is available on your node. If receipts look stale or mismatched, check for a reorg and re-fetch.
- Confirm method support with a latest-block call.
- Validate the block parameter form (number, tag, or 32-byte hash).
- Check for null results and distinguish pending from unavailable.
- Verify receipt.transactionHash against block.transactions[i].hash.
- Re-fetch on mismatch to handle reorgs.
- Check provider block-range and response-size limits for large blocks.
- Confirm archive capability for historical blocks.
- Log fallback usage and rate-limit errors for capacity planning.
Next Steps: Building a Receipt-Driven Pipeline
With bulk receipts in place, the next step is to decide how your pipeline handles realtime versus historical data. For realtime, consider a log-subscription design with a backfill for gaps; for historical analytics, bulk receipts per block are the efficient shape. The OnFinality Learn hub has related guides on logs, tracing, and monitoring that complement this one.
If you are choosing an endpoint, review Choosing an Ethereum RPC node and the Ethereum networks page. For access and pricing details, see RPC pricing and the API service page. Measure your own round-trip and rate-limit behavior before committing to a design, and keep the fallback path in place so your pipeline survives endpoints that lack the bulk method.
- Realtime: subscription plus backfill; historical: bulk receipts per block.
- Keep the per-transaction fallback for endpoints without the bulk method.
- Measure round trips and rate-limit behavior against your own endpoint.
- Review endpoint selection and pricing before scaling.