- Category
- Operations
- Reading time
- 12 minutes
- Published
- Updated
debug_traceTransaction vs trace_transaction: which trace API should you use?
EVM teams often ask for “transaction traces” as if every node returns the same artifact. In practice, debug_traceTransaction and trace_transaction belong to different RPC families, use different response shapes, and are not available on every client or chain. Replacing one method name with the other can break a debugger, indexer, accounting pipeline, or provider migration even when both calls describe the same transaction.
The right choice starts with the question the product must answer. A developer diagnosing one reverted call needs different data from an indexer searching internal value transfers across millions of blocks. This guide maps those jobs to the appropriate method, explains how to normalize the results, and gives you a production test plan for historical access and failover.
What is the short answer?
Use debug_traceTransaction with callTracer when you need a nested call tree for one known transaction: callers, callees, call types, inputs, outputs, gas use, value, and revert context. Use its default opcode logger only when you genuinely need step-by-step EVM execution; opcode traces can be much larger than call traces.
Use Parity-style trace_transaction when your downstream system is built around a flat list of localized call frames and traceAddress paths. Prefer the wider trace_* family when you also need operations such as trace_block, trace_filter, or replay calls that can return trace, VM-trace, or state-diff data.
Do not assume the two namespaces are interchangeable or universally enabled. First choose the output contract, then verify the exact method, chain, client, oldest transaction, tracer options, and failover path. If either namespace can satisfy the job, debug_traceTransaction with a native callTracer is usually the clearest starting point for single-transaction diagnosis; trace_* is often the better fit for a pipeline already modeled around Parity-style records or address-filtered trace retrieval.
What does debug_traceTransaction return?
debug_traceTransaction re-executes a mined transaction in its original block context. The result depends on the tracer configuration, so the method name alone does not define one stable schema.
Without a named tracer, Geth-style implementations normally return an opcode or struct log: program counter, opcode, remaining gas, gas cost, depth, and optionally stack, memory, storage, and return data for each EVM step. This is useful for low-level execution analysis, but enabling memory or retaining full stack and storage data can make responses very large. Geth's built-in tracer documentation recommends collecting only what the investigation needs.
With callTracer, the result is a nested call tree. Each frame can include call type, sender, recipient, value, gas, gas used, input, output, error, revert reason, and child calls. That shape is convenient for transaction debuggers, security analysis, and explaining a failed interaction. Other tracers answer different questions: prestateTracer can describe touched state or a state diff, while 4byteTracer collects invoked function selectors.
Tracer names, configuration fields, and named-tracer output still vary by implementation and version. The current draft Ethereum Execution APIs definition specifies the opcode result but explicitly leaves named-tracer output schemas outside its scope. Treat a configured tracer—not merely debug_traceTransaction—as the dependency you must qualify.
What does trace_transaction return?
Parity-style trace_transaction returns a flat array of trace entries for one mined transaction. A typical entry has an action, result or error, call or creation type, number of child traces, and a traceAddress array that locates the frame inside the call tree. Localized results also carry block and transaction identity.
The flat shape is not missing hierarchy. traceAddress is the path from the root: an empty array identifies the root frame, [0] its first child, and [0, 1] the second child beneath that frame. Consumers can reconstruct a tree, but they should preserve the path as the durable identity within the transaction.
The important advantage is the surrounding family. Official Reth trace namespace documentation separates ad-hoc replay methods from transaction-trace filtering methods. Depending on client support, you can request one transaction, all traces in a block, one frame by path, or address-filtered traces across a block interval. Replay methods can request combinations of a call trace, VM trace, and state diff.
That makes trace_* attractive for an indexer whose data model already expects flat, localized frames. It does not make it a universal tracing standard: Geth does not expose the Parity namespace, and EVM derivatives based on Geth, Bor, or Nitro commonly expose debug_* without trace_*.
Which method matches the question you are trying to answer?
Choose from the output backward:
- Why did this transaction revert? Start with
debug_traceTransactionpluscallTracer. Inspect the first failing frame, its input and output, and the parent path. A revert reason may be absent, so preserve raw return data for ABI-aware decoding. - Which contracts called which other contracts? A call tree is easiest to consume from
callTracer.trace_transactioncontains equivalent hierarchy throughtraceAddressif the chain exposes it. - Which opcodes, stack values, or storage accesses occurred? Use the debug opcode logger, with memory, stack, storage, return-data, and step limits set deliberately. A call trace cannot answer an opcode-level question.
- What state changed? Qualify
prestateTracerin diff mode or a replay method requestingstateDiff. The schemas differ; choose one and version your normalized model. - Which internal calls or value transfers involved an address over a range?
trace_filteris designed for address filters and block bounds when the client supports it. If onlydebug_*exists, you must first enumerate candidate transactions or blocks and trace them individually, which is a different workload. - I need every trace in a block. Compare
trace_blockwithdebug_traceBlockByHashordebug_traceBlockByNumber. Test response size, partial-failure representation, and transaction identity before choosing. - I need to simulate a transaction that has not been mined. Compare
debug_traceCallwithtrace_call; do not send a hypothetical call to a method that accepts only a mined transaction hash.
A receipt remains the cheaper first check for status, gas used, contract address, and emitted logs. Trace only when the product needs internal execution detail that receipts and logs do not contain.
Why can the two APIs disagree without either being wrong?
The same execution can be serialized differently. A nested call tree and a flat traceAddress list may contain the same call frames but order fields, errors, self-destruct actions, created contracts, and gas values differently. The debug opcode logger describes EVM steps, not just calls, so it will never resemble a Parity call list.
Normalize semantics instead of comparing raw JSON. At minimum, record chain ID, block number, block hash, transaction hash, transaction index, frame path, parent path, call type, from, to, created address, value, input, output, gas supplied, gas used, success or error, revert data, tracer family, client or provider, and collection time. Keep the raw response beside the normalized rows so a parser change does not require retracing history.
Define how each source maps child frames and failures before production. For callTracer, derive the frame path while walking the nested calls arrays. For Parity traces, retain traceAddress directly. Never use array position alone as durable identity, and never infer a successful transaction merely because one child frame has a result; inner calls can fail and be handled while the top-level transaction succeeds.
When validating two implementations, compare transaction status, frame paths, call types, addresses, values, inputs, outputs, and errors after normalization. Byte-for-byte response equality is the wrong acceptance criterion unless identical serialization is itself a product requirement.
Do historical traces require an archive node?
Tracing replays execution, so the node needs the transaction, its block context, and enough prior state to reconstruct the transaction's starting point. Geth's EVM tracing guide explains that this includes accessed account state, block metadata, and the intermediate state created by earlier transactions in the same block. If the required state is not immediately available, the client may re-execute from an earlier retained state; that can be slow or can fail when the necessary history is unavailable.
An archive configuration usually expands the historical range, but “archive RPC” is not proof that a specific trace method or tracer works. Clients retain and reconstruct state differently, namespaces may be disabled, transaction lookup or receipts may be pruned separately, and a provider's fallback may use another client. Test the oldest real transaction required by the product with the exact options you will use.
Also separate historical state from bulk trace indexes. A node may be able to replay one old transaction yet not implement trace_filter, while another client may expose address-filtered historical traces as a distinct capability. Ask whether the endpoint replays traces on demand, reads a trace index, or routes to another upstream only if that distinction affects limits and recovery; in all cases, verify the result rather than inferring it from the node label.
How should you qualify trace support before production?
Build a chain-method-tracer matrix and make it executable. Include recent and old successful transactions, reverts, contract creation, nested calls, delegate calls, precompiles, high-gas transactions, and a busy block. For every case, save an expected semantic result rather than only an HTTP status.
Probe each normal and failover path for:
- The exact namespace and method, including block and call variants.
- Required tracer name and
tracerConfigfields. - The oldest transaction and state depth the product needs.
- Nested versus flat response shape, frame ordering, errors, and revert data.
- Maximum acceptable response bytes and end-to-end deadline.
- Behavior when the transaction is unknown, history is pruned, the method is disabled, or execution exceeds its budget.
- Concurrency and catch-up rate for the real mix, not one isolated trace.
- Capability after the preferred upstream is unavailable.
Treat JSON-RPC -32601 as a capability result, not a transient outage to retry blindly. A fallback that supports standard reads but loses the required trace family is not a qualified fallback for that workload. Re-run the matrix after a client upgrade, provider migration, pruning change, or chain upgrade because the endpoint URL can stay constant while trace behavior changes.
How do you run trace workloads without causing an incident?
Trace calls are CPU-, storage-, and response-heavy compared with ordinary head reads. Give them their own queue, concurrency limit, deadline, retry budget, and observability. Do not let an indexer backfill consume every worker needed for user-facing transaction diagnosis.
Retry only transport failures or explicitly transient server failures, and keep one total deadline across attempts. Do not hedge a multi-second trace by default: sending the same replay to two nodes doubles expensive work. A timeout can be caused by an intrinsically heavy transaction, unavailable historical state, an overloaded client, or a provider budget; record which transaction, block, tracer, options, upstream, duration, and response bytes were involved before changing limits.
Cache successful traces by chain ID, block hash, transaction hash, tracer family, tracer configuration, and parser version. Block hash matters because a transaction can be reorganized into another block context. For unfinalized data, detect canonical-hash changes, invalidate orphaned results, and replay.
Monitor useful completions as well as requests: success and timeout rate by method and tracer, queue age, trace duration percentiles, response bytes, oldest failing block, head distance, retries per original request, and fallback share. Those signals distinguish slow EVM execution from capacity, history, and routing failures.
How do these trace families work on SolidRPC?
SolidRPC exposes HTTPS JSON-RPC and publishes debug and trace as separate per-chain capabilities in the live network catalog. Some chains support both families, some support debug_* only, and some expose standard RPC without tracing. Check the catalog and run the exact probe before committing to a method; a chain's archive label does not imply that both namespaces exist.
On selected networks, SolidRPC operates full or archive nodes and prefers healthy operated capacity. Qualified external capacity provides failover and covers networks or capabilities SolidRPC does not serve directly. Depending on the chain and method, a trace can therefore run on a SolidRPC-operated client or an external upstream. A production qualification must pass through the authenticated SolidRPC route so it exercises the routing and fallback behavior the application will actually use.
The gateway allows supported debug_trace* methods and Parity-style trace_* methods where the chain catalog advertises them. Trace and debug calls receive a longer server-side budget than standard methods and are not hedged; the current limits and examples are documented in the SolidRPC tracing guide. HTTPS remains available across the catalog. Activated WebSocket networks support ordinary RPC and native newHeads for eligible paid accounts. Each billable RPC call or head written consumes one shared response unit. WebSocket has live-only recovery and is excluded from monetary SLA credits. See the WebSocket guide.
Managed routing does not normalize the two response families for you. Keep your chosen schema, raw trace, reorg handling, cache key, and capability tests in the application.
EVM trace API decision checklist
- Write the product question before choosing a method: call tree, opcodes, state diff, address filtering, whole block, or simulation.
- Prefer
callTracerfor a readable single-transaction call tree. - Use the opcode logger only when step-level execution data is required, and disable fields you do not need.
- Choose
trace_transactionwhen the flat Parity schema andtraceAddressmatch the data model. - Require
trace_filterexplicitly if address-filtered range queries are part of the workload. - Store chain, block, transaction, and frame identity plus the raw response.
- Version tracer configuration and the normalization schema.
- Test recent, old, reverted, nested, created-contract, and high-gas transactions.
- Verify the exact method and oldest transaction through every failover path.
- Treat
-32601, pruned history, and trace timeout as different failure classes. - Isolate trace concurrency from standard RPC and avoid automatic hedging.
- Cache by block hash and invalidate orphaned traces after a reorganization.
- Re-run capability fixtures after client, provider, pruning, or chain changes.
The durable choice is not “debug or trace” in the abstract. It is a versioned contract between one chain, one method, one tracer configuration, one historical range, and one output schema that still holds when the preferred upstream fails.