Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
RPC Troubleshooting12 min read

Ethereum Transaction Tracing: trace_transaction vs debug_traceTransaction and trace_call

Understand Ethereum's trace and debug RPC families, when to use trace_transaction vs debug_traceTransaction, and how to run them.

TL;DR

Ethereum offers two distinct RPC families for execution tracing: the 'trace' namespace (trace_transaction, trace_call, trace_block) and the 'debug' namespace (debug_traceTransaction, debug_traceCall). They differ in output envelope, tracer selection, and client support. Choose trace_transaction for structured, OpenEthereum-style traces with built-in revert reasons and state diffs; choose debug_traceTransaction with callTracer for flexible, tracer-based output. Both re-execute the EVM against historical state, making them far heavier than eth_call. Availability and cost vary by provider, so always verify on your endpoint.

Direct Answer: Which Tracing RPC Should You Use?

When you need to understand what happened inside a mined Ethereum transaction—why it reverted, which internal calls were made, how gas was consumed—you have two main RPC families: the trace_* methods (from the OpenEthereum/Parity trace module, now supported by several clients) and the debug_* methods (from Geth's debug namespace). The short answer: use trace_transaction when you want a structured, opinionated trace with a consistent envelope (action/result) and built-in revert reasons; use debug_traceTransaction with a tracer like callTracer when you need flexibility in output format or access to opcode-level details. For simulating a not-yet-sent transaction, use trace_call (with optional state overrides) or debug_traceCall. Both families re-execute the EVM, so they are computationally expensive and often restricted by providers. Always check which methods your endpoint supports before building a pipeline.

This article explains the mechanism behind both families, compares their outputs, and provides a reproducible Node.js script to test your own endpoint. For a broader overview of Ethereum RPCs, see the OnFinality Learn hub and the Ethereum network page.

How EVM Tracing Works Under the Hood

Both trace_transaction and debug_traceTransaction work by re-executing the target transaction in an EVM instance against a specific state. For a mined transaction, that state is the historical state at the block where the transaction was included—this requires an archive node or a node with sufficient historical state. For trace_call and debug_traceCall, the state is the current head (or a specified block) plus optional state overrides that let you modify balances, code, or storage before execution.

During re-execution, the EVM records every opcode executed, every internal message call, contract creation, and state change. The difference lies in how that raw data is packaged. The debug_* namespace returns the output of a tracer—a piece of code that observes the EVM execution. The default tracer is the struct logger, which produces a verbose opcode-by-opcode log. More commonly, you specify callTracer to get a structured call tree, or prestateTracer to capture the pre-execution state. The trace_* namespace, on the other hand, has a fixed output schema defined by the OpenEthereum trace specification: each trace is an object with action, result, subtraces, and traceAddress, covering call, create, and suicide operations. It also offers methods like trace_filter to retrieve traces by address or block range, and includes revertReason and stateDiff in some implementations.

Because tracing re-executes the EVM, it is significantly heavier than a simple eth_call. The exact cost depends on the transaction's complexity and the tracer used. Many providers disable tracing by default or apply strict rate limits and longer timeouts. Always consult your provider's documentation—for example, RPC pricing and API service pages describe typical tiers. For a deeper dive into state overrides, see our guide on eth_call state overrides and simulation.

Comparing trace_transaction and debug_traceTransaction Outputs

To illustrate the difference, consider a simple transaction that transfers ETH and calls a contract. trace_transaction returns an array of trace objects, each with a type (call, create, suicide), action (from, to, value, gas, input), and result (output, gasUsed). It also includes traceAddress to represent the call tree. Here's a simplified example:

In contrast, debug_traceTransaction with callTracer returns a nested JSON object representing the call tree, with fields like type, from, to, value, gas, gasUsed, input, output, and calls (an array of child calls). It does not include traceAddress because the nesting itself encodes the structure. Example:

The trace_* namespace also offers trace_filter to query traces by address or block range, which is useful for indexers. The debug_* namespace does not have a direct filter; you must trace individual blocks or transactions. For a comprehensive comparison of client support, refer to the official Geth debug API documentation and the Erigon trace API documentation. Availability varies by client and endpoint, so always verify.

[
  {
    "action": {
      "callType": "call",
      "from": "0x...",
      "gas": "0x7a120",
      "input": "0x...",
      "to": "0x...",
      "value": "0x0"
    },
    "result": {
      "gasUsed": "0x5208",
      "output": "0x"
    },
    "subtraces": 1,
    "traceAddress": [],
    "type": "call"
  },
  {
    "action": {
      "callType": "call",
      "from": "0x...",
      "gas": "0x...",
      "input": "0x...",
      "to": "0x...",
      "value": "0x0"
    },
    "result": {
      "gasUsed": "0x...",
      "output": "0x..."
    },
    "subtraces": 0,
    "traceAddress": [0],
    "type": "call"
  }
]

{
  "type": "CALL",
  "from": "0x...",
  "to": "0x...",
  "value": "0x0",
  "gas": "0x7a120",
  "gasUsed": "0x5208",
  "input": "0x...",
  "output": "0x",
  "calls": [
    {
      "type": "CALL",
      "from": "0x...",
      "to": "0x...",
      "value": "0x0",
      "gas": "0x...",
      "gasUsed": "0x...",
      "input": "0x...",
      "output": "0x..."
    }
  ]
}

When to Use trace_call vs debug_traceCall

trace_call and debug_traceCall are used to simulate a transaction without sending it to the network. They accept a transaction object (from, to, gas, gasPrice, value, data) and an optional block number or tag. The key advantage of trace_call is that it returns a structured trace similar to trace_transaction, and it supports state overrides via the stateOverrides parameter (in some implementations). This is ideal for simulating a contract interaction to estimate gas, check for reverts, or inspect internal calls before broadcasting.

debug_traceCall is the debug-namespace equivalent. It also accepts a transaction object and a tracer argument. With callTracer, it returns the same call-tree structure as debug_traceTransaction. The choice between them often comes down to which namespace your provider supports. Some providers only expose one. For example, if you are using a Geth node, debug_traceCall is available; if you are using an OpenEthereum-compatible endpoint, trace_call is the way to go.

When simulating, you can also use eth_call with state overrides, but that only returns the output or revert reason, not the internal call tree. For a detailed guide on state overrides, see eth_call state overrides and simulation.

Reproducible Example: Testing Your Endpoint with Node.js

The following Node.js script lets you test which tracing methods your RPC endpoint supports and compare the outputs of trace_transaction and debug_traceTransaction on a real transaction. It also runs a trace_call simulation with a state override. You will need a node.js environment with the axios library installed (npm install axios). Replace YOUR_RPC_URL with your endpoint URL, and optionally provide a transaction hash and a contract address for the state override test.

The script performs three requests: (1) trace_transaction on a given hash, (2) debug_traceTransaction with callTracer on the same hash, and (3) trace_call with a simple transfer to a contract address, using a state override to set the contract's balance. It prints the results and a summary of which methods succeeded. Note that if a method is not supported, the node will return an error; the script catches that and reports it.

Run the script and fill in the results table below. This will help you understand what your endpoint supports and the shape of the responses.

const axios = require('axios');

const RPC_URL = 'YOUR_RPC_URL'; // e.g., https://mainnet.example.com
const TX_HASH = '0x...'; // replace with a real transaction hash
const CONTRACT_ADDRESS = '0x...'; // replace with a contract address for state override

async function rpcCall(method, params) {
  const response = await axios.post(RPC_URL, {
    jsonrpc: '2.0',
    id: 1,
    method,
    params
  });
  if (response.data.error) {
    throw new Error(response.data.error.message);
  }
  return response.data.result;
}

async function main() {
  // 1. trace_transaction
  try {
    const trace = await rpcCall('trace_transaction', [TX_HASH]);
    console.log('trace_transaction succeeded. Number of traces:', trace.length);
    console.log('First trace type:', trace[0]?.type);
  } catch (e) {
    console.log('trace_transaction failed:', e.message);
  }

  // 2. debug_traceTransaction with callTracer
  try {
    const debug = await rpcCall('debug_traceTransaction', [TX_HASH, { tracer: 'callTracer' }]);
    console.log('debug_traceTransaction succeeded. Top-level type:', debug.type);
    console.log('Calls count:', debug.calls ? debug.calls.length : 0);
  } catch (e) {
    console.log('debug_traceTransaction failed:', e.message);
  }

  // 3. trace_call with state override
  try {
    const tx = {
      from: '0x0000000000000000000000000000000000000000',
      to: CONTRACT_ADDRESS,
      value: '0x0',
      data: '0x' // change to a function call if needed
    };
    const overrides = {
      [CONTRACT_ADDRESS]: {
        balance: '0xde0b6b3a7640000' // 1 ETH
      }
    };
    const result = await rpcCall('trace_call', [tx, ['trace'], overrides]);
    console.log('trace_call succeeded. Output:', result.output);
    console.log('Gas used:', result.gasUsed);
  } catch (e) {
    console.log('trace_call failed:', e.message);
  }
}

main();

Results Table: Fill in Your Endpoint's Behavior

Use this table to record the outcome of each method on your target endpoint. This is a diagnostic tool, not a benchmark.

  • trace_transaction – Supported? (Yes/No) – Error message if any – Number of traces returned – First trace type
  • debug_traceTransaction (callTracer) – Supported? (Yes/No) – Error message if any – Top-level type – Number of child calls
  • trace_call (with state override) – Supported? (Yes/No) – Error message if any – Output (first 10 bytes) – Gas used
  • Notes – Any rate limits, timeouts, or special requirements observed

Troubleshooting Common Tracing Failures

When tracing fails, the error message often points to the root cause. Here are common issues and how to fix them:

  • Method not found – The endpoint does not support the trace_* or debug_* namespace. Check your provider's documentation or switch to a node that supports it. Some providers offer separate endpoints for tracing.
  • Historical state not available – Tracing an old transaction requires archive node data. If you get an error like 'missing trie node' or 'header not found', your node may be a full node without archive data. Use an archive endpoint or a provider that offers historical traces.
  • Timeout – Tracing is slow. If your request times out, try a more specific tracer (e.g., callTracer instead of the default struct logger), or use a provider with longer timeouts. See our guide on Ethereum RPC timeouts and retries.
  • Rate limiting – Providers often rate-limit tracing methods more aggressively. If you hit rate limits, consider batching requests or using a dedicated tracing endpoint. Check RPC pricing for typical limits.
  • Invalid tracer name – When using debug_traceTransaction, ensure the tracer name is supported by your client. Common tracers include callTracer, prestateTracer, 4byteTracer, and opcodeLogger. Refer to the Geth documentation for a list.
  • State override format – In trace_call, the stateOverrides parameter must be an object keyed by address, with each value containing optional balance, code, nonce, or state fields. Incorrect formatting can cause errors.

Limitations and Tradeoffs

Tracing is a powerful but expensive operation. It can consume significant CPU and I/O, especially for complex transactions or large blocks. Providers often disable tracing by default or offer it as a premium feature. Always check the documentation of your specific endpoint. For example, some providers only support debug_traceTransaction on recent blocks, while others require you to use a dedicated archive node.

The output format also varies between clients. While the trace_* namespace aims for consistency with the OpenEthereum spec, there are subtle differences in field names and availability of revertReason or stateDiff. Similarly, debug_* tracers can produce different structures depending on the client version. Always validate your parsing logic against actual responses.

For production systems, consider caching trace results if you need to replay the same transaction multiple times. Also, be mindful of the payload size: a trace for a complex transaction can be several megabytes, which may impact network performance. Use filters or tracers that limit output when possible.

If you are building an indexer or analytics pipeline, you might prefer trace_filter to retrieve traces for a specific address or block range, but this method is not available on all clients. For more on monitoring and endpoint health, see Monitoring RPC endpoints and node health.

Next Steps and Further Reading

Now that you understand the difference between the trace and debug namespaces, you can choose the right method for your use case. To deepen your knowledge, explore the following resources:

For authoritative references, see the Ethereum execution APIs documentation and the Geth debug namespace documentation. If you are using a provider, always consult their specific tracing documentation, as support and output may vary.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started