A "top trace" is the outermost call frame in a transaction's execution trace — the entry point from which every internal call, revert, and log descends. Reading the top trace first tells you whether a failure happened in the transaction's own logic, in a nested contract call, or before execution even began. This article explains the mental model behind trace and debug RPC methods, gives a runnable procedure for capturing and comparing traces across providers, and shows how to turn a raw trace into a diagnosis. It separates documented node behavior from the measurements you should run yourself against your own RPC endpoint.
What a top trace actually represents
When a transaction executes on an EVM chain, the node records a tree of call frames: the transaction's entry call, every contract call it makes, every delegatecall, staticcall, and create, plus the return or revert status of each. The top trace is the root of that tree — the frame corresponding to the transaction itself, with the to address, input data, gas supplied, and the final status of the whole execution.
This matters because most transaction failures are reported to you as a single opaque string. A wallet or dapp sees execution reverted with no reason, or a transaction that never mines, and the top-level error tells you nothing about which nested call failed. The trace is the only reliable way to walk from that symptom down to the frame that actually reverted.
The term is also used loosely for the first trace returned by a trace API when you query an address or block. In this article it means the root frame of a single transaction's execution, which is the interpretation that makes the diagnostic procedure reproducible.
- Top trace = root call frame of one transaction's execution.
- Child frames = internal calls, delegatecalls, creates, and their results.
- Revert reason, if any, usually lives in the deepest failing frame, not the root.
Assumptions and prerequisites
You need an RPC endpoint that exposes trace or debug namespaces. Not every provider enables them: debug_* and trace_* methods are expensive, can be disabled by default, and are often rate-limited or restricted to archive nodes. Before building a workflow around tracing, confirm which methods your endpoint actually serves.
You also need the transaction hash of a transaction you can already see on-chain, or a pending transaction you are trying to diagnose. For pending transactions, tracing depends on the node's mempool visibility, which varies by provider and is not guaranteed.
Finally, treat tracing as a read-only diagnostic. It does not resubmit or modify anything, but it can be slow and heavy, so avoid running it in tight loops against a shared endpoint. If you need sustained trace access, a dedicated node is the appropriate place to run it — see dedicated node options for how that is provisioned.
How trace and debug methods differ
There are two families of tracing RPC methods, and they are not interchangeable. The debug_* family is Geth-style: debug_traceTransaction returns a nested call tree (or a flat opcode log, depending on tracer configuration). The trace_* family is Parity/OpenEthereum-style: trace_transaction returns a flat list of traces with traceAddress paths that you reassemble into a tree.
Both describe the same execution, but the shapes differ. A debug_traceTransaction response with the default call tracer nests children inside calls. A trace_transaction response gives you an array where each entry has a traceAddress like [0, 1] meaning "second child of the first child of the root." The root itself has an empty traceAddress.
Which one you get depends on the client behind the endpoint. Geth and Erigon expose debug_*; Erigon and Nethermind can expose trace_*; some providers expose both, some neither. Check the method list for your endpoint before writing code against it.
curl -s -X POST RPC_URL \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"debug_traceTransaction","params":["0xYOUR_TX_HASH",{"tracer":"callTracer"}]}'Capturing the top trace with debug_traceTransaction
The callTracer variant is the most readable starting point because it returns a nested structure you can inspect directly. The root object is your top trace: it has type, from, to, value, gas, gasUsed, input, output, and either calls or an error field.
If the root object has an error and no calls, the failure happened in the top frame itself — for example, a revert in the entry function before any external call. If the root has calls and one of them carries the error, the failure is nested and you need to descend.
The following Python snippet fetches the trace and prints the root frame plus the first failing child, which is usually enough to identify the culprit:
import json, urllib.request
RPC_URL = "RPC_URL"
TX_HASH = "0xYOUR_TX_HASH"
payload = json.dumps({
"jsonrpc": "2.0", "id": 1,
"method": "debug_traceTransaction",
"params": [TX_HASH, {"tracer": "callTracer"}],
}).encode()
req = urllib.request.Request(RPC_URL, data=payload,
headers={"Content-Type": "application/json"})
root = json.load(urllib.request.urlopen(req))["result"]
print("root:", root.get("type"), root.get("to"), root.get("error"))
def walk(frame, depth=0):
for child in frame.get("calls", []):
if child.get("error"):
print(" " * depth, "fail:", child.get("type"),
child.get("to"), child.get("error"))
walk(child, depth + 1)
walk(root)Reassembling a top trace from trace_transaction
If your endpoint exposes trace_transaction instead, you get a flat array. The top trace is the entry whose traceAddress is []. Every other entry is a descendant, and the traceAddress array is the path from the root.
To reconstruct the tree, sort by traceAddress length and attach each entry to its parent path. This is mechanical but worth doing once in a helper, because reading a flat list by eye is error-prone when a transaction makes dozens of calls.
The same diagnostic rule applies: find the deepest entry with an error field, then read its revertReason if present. The root entry's error will often just say Reverted while the child carries the actual reason string.
curl -s -X POST RPC_URL \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"trace_transaction","params":["0xYOUR_TX_HASH"]}'A reproducible comparison procedure
Because trace support and trace fidelity vary by endpoint, the honest way to choose one is to measure it against your own workload. Run the same set of transaction hashes through each candidate endpoint and record the outcome in a table like the one below. Do not fill in numbers from this article — the cells are for your measurements.
| Endpoint label | Method tested | Root frame returned | Deepest failing frame found | Latency (ms) | Error / notes |
|---|---|---|---|---|---|
| RPC_URL_A | debug_traceTransaction | ||||
| RPC_URL_B | debug_traceTransaction | ||||
| RPC_URL_C | trace_transaction |
Pick a fixed sample of transactions: some that succeeded, some that reverted in the top frame, some that reverted in a nested call, and some that are still pending. Run each endpoint against the same sample, at roughly the same time, and record whether the trace was returned at all, whether the failing frame was present, and how long the call took. Repeat the run a few times so a single slow response does not dominate your conclusion.
The comparison is only meaningful if the endpoints are serving the same chain and the same block height. If one endpoint is behind, its trace for a recent transaction may be missing or inconsistent, which is a finding in itself.
Reading the result: what a healthy trace looks like
A successful trace has a root frame with no error, a gasUsed less than or equal to gas, and a tree of child frames that all return without error. The output of the root is the return data of the transaction's entry call.
A reverted trace has exactly one failing frame in the common case, and the frames above it are marked as reverted because the failure propagated upward. If you see multiple failing siblings, the transaction likely used a try/catch pattern or a multicall that tolerated individual failures — read the parent frame's logic before concluding the transaction failed.
A trace that returns an empty or malformed result usually means the endpoint does not support the method, the transaction is too old for a non-archive node, or the node pruned the state needed to re-execute it. Those are endpoint problems, not contract problems.
Troubleshooting common trace failures
The most common failure is method not found or method not supported. That is a provider configuration issue, not a bug in your code. Confirm the method list for your endpoint, and if tracing is not available, either switch endpoints or move the workload to a node you control.
The second most common is missing trie node or a state-pruning error. Tracing re-executes the transaction against historical state, so it needs an archive node for anything older than the pruning window. If you are tracing recent transactions and still see this, the endpoint may be load-balanced across nodes with different retention.
The third is timeout. Trace calls are heavier than eth_call and can exceed default HTTP timeouts on large transactions. Increase your client timeout and avoid parallelizing trace requests against a shared endpoint. For high-volume tracing, a dedicated node removes the shared rate limit entirely; see RPC pricing for how capacity is metered.
- Method not found → endpoint does not expose trace/debug namespace.
- Missing trie node → non-archive node or pruned state.
- Timeout → large transaction or shared rate limit; retry with backoff or dedicated capacity.
- Empty result → transaction not yet indexed or node behind head.
Limitations and tradeoffs
Tracing is not a substitute for reading contract source. A trace tells you which frame reverted and with what reason, but not why the contract's logic chose that path. Pair the trace with the verified source and the input calldata.
Trace output is also client-dependent. Geth's callTracer and Erigon's trace_transaction describe the same execution but differ in field names, nesting, and how they represent delegatecalls and creates. Any tooling you build should normalize the shape rather than assume one client's format.
Finally, tracing pending transactions is best-effort. Mempool visibility differs by provider, and a transaction you can see in one node's mempool may be invisible to another. Do not build a production workflow that depends on tracing unconfirmed transactions unless you control the node.
Next steps
Start by confirming which trace methods your current endpoint serves, then run the comparison procedure above against a small fixed sample. If tracing is unavailable or unreliable, move that workload to a dedicated node and keep the shared endpoint for ordinary reads.
For the broader picture of how RPC endpoints are structured and what each namespace costs, see the RPC endpoints guide and the multi-chain RPC endpoints guide. To compare networks before you commit, browse supported networks. And if you would rather have the endpoint choice made for you, the RPC Assistant can recommend a concrete provider and network configuration for your tracing workload.