The Real Cost of `debug_traceTransaction` (and When to Use It)
debug_traceTransaction is one of the most powerful methods in the Ethereum RPC surface. It replays a transaction with full state visibility — every opcode executed, every contract called, every state change, every reverted internal call. If you've ever wondered "what did this transaction actually do?", trace is the answer.
It's also one of the most expensive methods you can call. On Alchemy, a single debug_traceTransaction costs 309 Compute Units vs 15 for eth_getTransactionReceipt. On Infura, it's similar. Tracing a few thousand transactions can blow through a month's allowance in an afternoon.
The practical question isn't "is trace useful?" — it obviously is. It's "when does this expensive method actually justify itself, vs when does a cheaper call give you the same answer?" This post is that decision tree, with the four tracer modes and what each is actually for.
What debug_traceTransaction actually does
When you call it, the node:
- Loads the state at the block before the transaction
- Replays every transaction in that block up to (but not including) the one you want to trace
- Executes your target transaction step-by-step, recording every opcode and state change
- Returns the recorded execution trace
That's the cost basis — it's not just reading data, it's re-executing the transaction. For deep contract calls or trace-heavy workloads (large DEX swaps, multi-hop bridges, complex MEV transactions), the node may do significant CPU work.
That's why providers charge so much. It's not arbitrary — they're amortizing real compute load.
The four tracer modes
debug_traceTransaction takes a second argument that controls what it records. This matters a lot for cost and response size.
callTracer — the one you almost always want
const trace = await client.request({
method: "debug_traceTransaction",
params: [
"0xabc123...",
{ tracer: "callTracer" },
],
});
Returns the call tree: which contract was called, with what calldata, returning what, with sub-calls nested inside. Doesn't include the opcode-level steps — just the contract-to-contract structure.
For most "what did this transaction do?" debugging needs, callTracer is what you want. Response is JSON, often a few KB. Latency is moderate.
prestateTracer — when you need state snapshots
const trace = await client.request({
method: "debug_traceTransaction",
params: [
"0xabc123...",
{ tracer: "prestateTracer" },
],
});
Returns the state of every account the transaction touched, before it executed. Useful for:
- Reconstructing what storage slots were read
- Building "fork at this transaction" testing environments
- Verifying state assumptions
More expensive than callTracer because the node serializes all touched account state.
Default (struct log) tracer — opcode-level, very expensive
const trace = await client.request({
method: "debug_traceTransaction",
params: ["0xabc123..."], // no tracer arg = struct log mode
});
Returns every opcode executed with stack, memory, and storage diff at each step. For a moderate transaction this can be tens of MB of JSON. Latency is significant.
This is the mode that gives debug_traceTransaction its scary cost reputation. Avoid unless you specifically need opcode-level debugging — which is rare outside of contract authoring.
4byteTracer and custom JS tracers
4byteTracer returns just the 4-byte function selectors called. Lightweight, fast, useful for analyzing call patterns without the full data.
Custom JS tracers let you define exactly what gets recorded. Powerful but provider-specific — not every provider exposes the tracer field allowing arbitrary JS. Most do support the named tracers above.
Cost comparison across providers
For a single debug_traceTransaction call with callTracer:
| Provider | Cost | Available on free tier? |
|---|---|---|
| Alchemy | 309 CU (~$0.0004 at Growth overage) | Yes, but burns budget fast |
| Infura | 309 CU (~$0.0004) | Yes, daily cap is tight |
| QuickNode | 25 API credits | Yes |
| SwiftNodes | 1 request | Yes (rate-limited like any other call) |
| Public RPC | — | Usually disabled entirely |
The "available on public RPCs" column matters: debug_* methods are almost always disabled on public/unauthenticated endpoints. So if you need traces, you need a paid provider that explicitly enables them. SwiftNodes, Alchemy, Infura, QuickNode, and Chainstack all do. Public/community RPCs typically don't.
When you actually need trace
Trace is the right tool for these cases:
1. "Why did this transaction revert?"
eth_getTransactionReceipt tells you a tx reverted but doesn't say why. callTracer shows the entire internal call tree, including the specific subcall that reverted and its error message. Cheaper alternative: eth_call against the same block re-runs the transaction and returns the revert reason — but only if the transaction can be deterministically re-executed (no oracles updating between original and replay, etc.).
2. MEV analysis / sandwich detection
Need to see what every subcall did inside a complex transaction. callTracer is essential here. There's no shortcut — the transaction receipt only tells you the top-level outcome, not the internal flow that an MEV searcher cares about.
3. Reconstructing internal transfers (ETH and token)
Internal ETH transfers (CALL with value > 0) aren't visible in receipts or logs. Token transfers ARE in logs (the Transfer event), but native ETH movement isn't. To see who got how much ETH from a complex transaction, you need callTracer.
4. Forensics on hacks
When you're auditing what happened in an exploit, opcode-level detail matters. Struct log tracer is sometimes worth its cost. This is a niche use case but legitimate.
5. Building accurate "what would this transaction do?" simulators
Tenderly, Foundry's cast run, and similar tools use trace internally. If you're building tooling like this, you need real trace data.
When you do NOT need trace (use these instead)
This is the under-appreciated part. Many "I'll just trace it" instincts have cheaper alternatives:
"Did transaction X succeed?"
Use eth_getTransactionReceipt. The status field is 1 (success) or 0 (revert). Cost: 15 CU on Alchemy vs 309 for trace. 20× cheaper.
"What events did transaction X emit?"
Receipts include the events in the logs field. Same cost as above. No trace needed.
"What was the gas used?"
In the receipt: gasUsed.
"What's the balance change for address X from transaction Y?"
If it's a token: parse the Transfer events in the receipt logs. If it's native ETH and the address is the tx sender or receiver: derive from value + gasUsed * effectiveGasPrice. The only case where trace is required is internal ETH transfers to a third address — that's a real trace-required case but rarer than people assume.
"What's the function signature this transaction called?"
The transaction's input data field starts with 4 bytes — the function selector. Decode it with 4byte directory or similar. Receipt cost. No trace.
"Did this transaction touch contract X?"
If "touch" means "called," the events emitted by contract X will appear in the logs. If "touch" means "read storage from," that's harder — but you almost never need that, and if you do, prestateTracer is the right tool (not full opcode trace).
"What block did transaction X land in?"
eth_getTransactionByHash. Cost: ~17 CU.
The honest decision tree
A practical heuristic for whether to reach for trace:
Can `eth_getTransactionReceipt` answer this?
├── Yes → use that (15 CU)
└── No
Does callTracer answer it?
├── Yes → use callTracer (309 CU)
└── No
Do you need opcode-level detail?
├── Yes → struct log tracer (expensive, justified)
└── No → you don't actually need trace
The most common mistake: reaching for trace when you actually just need to parse the receipt's logs. The receipt has the event data, the gas data, the success/failure flag, and (via the transaction object) the calldata. That covers ~80% of "what did this transaction do?" questions for free vs trace's per-call cost.
When the receipt isn't enough but trace is overkill
For a middle ground, eth_call with the historical block tag re-executes a transaction in isolation without recording every step. It's cheaper than trace and tells you the return value or revert reason:
// Replay tx at the block where it was originally sent, get back the result/revert reason
const result = await client.call({
to: txInfo.to,
data: txInfo.input,
value: txInfo.value,
blockNumber: txInfo.blockNumber,
});
This catches a lot of "why did this revert?" cases without paying the trace tax. The caveat: it doesn't show internal subcalls or state changes — just the top-level outcome.
Indexer alternatives
For at-scale trace workloads — say, you need every internal call for every transaction in a contract's history — calling debug_traceTransaction per tx is the wrong tool. Even at $0.0004 per call, 10M transactions is $4,000.
Dedicated tracing services exist:
- Otterscan — open source, designed for trace-heavy block explorer use
- Tenderly — paid, comprehensive trace + simulation tooling
- Sentio — paid, indexer + alerts using trace data
- Subsquid — has trace integration in its indexer framework
For one-off debugging, trace via your provider. For systematic historical trace analysis, use a service that's already extracted and indexed it.
The takeaway
debug_traceTransaction is the right answer for: revert root-cause analysis, MEV breakdowns, internal ETH transfers, exploit forensics. It is the wrong answer for: things you can read from the receipt's status, logs, and gasUsed fields — which covers most "what did this transaction do?" workflows.
The 20× cost difference between trace and receipt is real. For one-off debugging it doesn't matter. For workloads doing this per-transaction in production, it matters a lot.
SwiftNodes supports debug_traceTransaction with all standard tracers (callTracer, prestateTracer, 4byteTracer, struct log) on every paid plan, no separate "trace tier" upgrade. See Ethereum RPC details or grab a free API key — no credit card, no KYC.
John Sullivan covers RPC infrastructure, node operations, and multi-chain development at SwiftNodes — what it actually takes to keep endpoints fast, fresh, and reliable across EVM and non-EVM networks.
Related posts
- trace_filter and trace_transaction: Reading What Receipts Can't Show
Receipts show that a transaction succeeded; traces show what it did. How trace_transaction and trace_filter expose internal calls, where the Parity trace namespace actually works in 2026 (we probe it weekly — it's 11 of 54 EVM chains), and the param quirks that trip people up.
- JSON-RPC Error Codes, Decoded: Retry, Fix, or Accept?
-32601, -32000, code 3, HTTP 429 — every RPC error belongs to one of three families: try another node, fix your request, or accept the chain's answer. A field guide with real error payloads.
- eth_getProof: Verify Blockchain State Without Trusting Your RPC Node
Every eth_call answer is just something a server told you. eth_getProof returns state with a Merkle proof you can check yourself — the primitive behind bridges, light clients, and trust-minimized reads.