debug_traceBlockByNumber: Trace Every Transaction in a Block

debug_traceBlockByNumberTrace methodEthereum JSON-RPC

debug_traceBlockByNumber re-executes an entire block with tracing enabled and returns one trace per transaction. It is the method behind MEV analysis, internal-transaction indexing and any pipeline that needs to know what really happened in a block rather than what the receipts summarise. It is also, by a wide margin, one of the largest responses an Ethereum node will ever hand you.

1 credit
per call on BLAZED.sh
Trace
call type
2
parameters
Geth, Nethermind, Erigon, Reth
client support

Try debug_traceBlockByNumber

Request as curl
curl -s http://localhost:8545 \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "debug_traceBlockByNumber",
  "params": [
    "0x18789ac",
    {
      "tracer": "callTracer",
      "tracerConfig": {
        "onlyTopCall": true
      }
    }
  ]
}'

Shared endpoints keep the debug namespace closed, so this request returns "the method debug_traceBlockByNumber does not exist/is not available" against a public URL. Run the curl against a node with debug APIs enabled, such as your own or the one a BLAZED.sh container sits on, and redirect the output to a file: even with onlyTopCall the answer is large.

What debug_traceBlockByNumber does

The node loads the state of the parent block and replays every transaction in order with tracing hooks attached. Doing that once for the whole block is dramatically cheaper than issuing debug_traceTransaction for each transaction separately, because the expensive part, reconstructing and warming the state, happens once instead of two hundred times.

The options object is the same one debug_traceTransaction takes, and the same rules apply: name a tracer, or receive an opcode-level struct log for every transaction in the block. callTracer is the usual choice, with tracerConfig.onlyTopCall when you want the shape of the block rather than the interior of every call.

The response is an array in block order, one entry per transaction, each pairing the transaction hash with its trace result. A transaction whose trace failed carries an error instead, so a partial failure does not sink the whole response. Its siblings are debug_traceBlockByHash, which pins a specific block and is the reorg-safe choice, and the Parity-style trace_block and trace_replayBlockTransactions.

Parameters

#NameTypeDescription
1blockParameterstringHex block number or one of latest, safe, finalized, earliest. Tracing pending is not meaningful; use debug_traceCall to simulate instead.
2optionsoptionalobjecttracer (callTracer, prestateTracer, 4byteTracer or a custom tracer), tracerConfig ({ onlyTopCall, withLog, diffMode }), timeout such as "60s", and reexec, how many blocks back the node may replay to rebuild the required state.

What it returns

An array with one entry per transaction, in the order they were executed, each holding the transaction hash and the trace produced by your chosen tracer. Entries whose tracing failed carry an error field instead of a result, which is worth checking rather than assuming a uniform array.

Sizes are the defining characteristic. A busy mainnet block traced with callTracer runs to megabytes; the same block with the default struct logger is orders of magnitude larger and will usually hit a timeout before it finishes serializing.

Example response

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "txHash": "0x20f83f863231a8e1c97f14d1b2e9379d389c57d3bb82d265cd6e5d806378ba9b",
      "result": {
        "type": "CALL",
        "from": "0xca112ee75e517fdbad2a7450c65cdaab8256d9a2",
        "to": "0x3f2b113d180ecb1457e450b9efcac3df1dd29ad3",
        "gasUsed": "0x317fa",
        "input": "0x3c83980f...",
        "output": "0x"
      }
    },
    {
      "txHash": "0x2efde06bb1dcf26741f3a7c1385d5a62ad2554523322e9dd53f1635ba6add6d1",
      "result": { "type": "CALL", "gasUsed": "0x8238e", "…": "…" }
    }
  ]
}

Two entries out of 209, with onlyTopCall keeping each one to a single frame. Drop that option and every entry grows a nested calls tree; the block goes from a readable summary to megabytes of JSON.

debug_traceBlockByNumber with ethers.js

import { WebSocketProvider } from "ethers";

// inside a BLAZED.sh container the node is one local socket away
const provider = new WebSocketProvider("ws://eth:8545");

// trace the head block: one request instead of one per transaction
const head = await provider.getBlockNumber();
const traces = await provider.send("debug_traceBlockByNumber", [
  "0x" + head.toString(16),
  { tracer: "callTracer", timeout: "60s" },
]);

const failed = traces.filter((t) => t.result?.error);
console.log(traces.length, "transactions,", failed.length, "reverted");

// walk one call tree
const count = (frame) => 1 + (frame.calls ?? []).reduce((n, c) => n + count(c), 0);
console.log("frames in tx 0:", count(traces[0].result));

Gotchas and common errors

One block trace beats N transaction traces

Tracing a block transaction by transaction makes the node rebuild and re-warm state for every single call, which is why a loop of debug_traceTransaction over a block is so much slower than a single block trace. If you are indexing internal calls, this method is the correct primitive, and debug_traceBlockByHash is the version to use when you need to be sure which block you got.

Budget for the response size

callTracer on a full mainnet block produces megabytes, and the default struct logger produces amounts of data that are not practical to move over an HTTP response at all. Set onlyTopCall when you only need the shape of the block, stream and parse incrementally rather than buffering, and expect the transfer itself to dominate the time on anything but a local connection.

Raise the timeout, then raise it again

Clients bound how long a tracer may run and abort with a timeout error when a heavy block exceeds it. Passing timeout: "60s" in the options is routine for whole-block traces. If it still times out, the answer is usually a lighter tracer or onlyTopCall rather than a longer deadline.

Old blocks need state that may be gone

The node needs the state of the parent block. On a full node that means recent blocks trace fine and older ones fail with "required historical state unavailable" unless reexec lets it replay far enough back, which is slow. Systematic historical analysis is an archive workload; on BLAZED.sh archive access is an Enterprise feature and standard plans trace at the tip.

Check every entry for an error

Individual transactions can fail to trace while the rest of the block succeeds, so the array is not guaranteed to be uniform. Code that maps straight over result assuming a trace object will throw on the first entry that carries an error instead. Note also that a trace error is not a transaction revert: a reverted transaction traces perfectly well, with the revert visible inside the frame.

The array is not the receipts list

Traces come back in execution order and align with the block's transaction list by index, but they are a different view of the block than receipts. Gas figures in a trace are per call frame; the gas the sender paid is in the receipt. Analytics that mix the two need to be explicit about which number they mean.

What debug_traceBlockByNumber costs on BLAZED.sh

debug_traceBlockByNumber costs 1 credit per call on BLAZED.sh, the same as any standard method; the 4-credit rate is reserved for txpool_content and the two trace_replay methods. What makes this call expensive is the size surcharge: responses over 100KB add 50 credits per MB, and a whole-block callTracer response is measured in megabytes, so this is the method where that rule genuinely bites. onlyTopCall is not just a speed knob, it is a billing one. It is also the workload where a co-located node matters most, since multi-megabyte responses over a local socket never touch the public internet.

See the full credit price list

debug_traceBlockByNumber: frequently asked questions

Is debug_traceBlockByNumber faster than tracing each transaction?

Considerably. The node reconstructs the parent state once and replays the block in order, instead of repeating that setup for every transaction. For indexing internal calls it is the right primitive.

Why does my whole-block trace time out?

The tracer exceeded the client's time budget. Pass a longer timeout such as "60s", set tracerConfig.onlyTopCall to skip child frames, and never leave the tracer unset, since the default struct logger is not practical at block scale.

Do I need an archive node?

Not for recent blocks, whose parent state a full node still holds. For systematic historical tracing you do, and on BLAZED.sh archive access is an Enterprise feature while standard plans trace tip-of-chain blocks.

What is the difference between debug_traceBlockByNumber and trace_block?

They are the two API families for the same job. The debug method is the Geth-style interface with named tracers and nested call trees; trace_block is the Parity-style interface returning flat traces with traceAddress paths. Pick one family per pipeline, since the schemas do not interchange.

Can I trace the pending block?

Not usefully. The pending block is this node's local projection and changes between calls. To reason about a transaction that has not been mined, simulate it with debug_traceCall instead.

Call debug_traceBlockByNumber from the node itself

Deploy your container or script next to a synced Ethereum node. No rate limits, no compute units, sub-10ms local RPC.