Data access · 10 min read
Internal transactions explained: traces and the cost of getting them
Most of what happens on an EVM chain happens inside transactions, not as transactions. A swap routed through three contracts is one submitted transaction and a tree of internal calls underneath it. Those internal calls are what block explorers label internal transactions, and they are missing from the two data types most teams start with. This guide explains what they are, why a transaction list and event logs both miss them, what a trace record holds, and why traces are the one data type that is genuinely hard to get from a node. Every figure is from a real query against SQD's Portal over a fixed block range.
1. What an internal transaction is
The name is a small lie that stuck. An internal transaction is not a transaction. Only the top-level call, the one an account signs and pays gas for, is a real transaction. Everything that call triggers as it runs, one contract calling another, a contract forwarding ETH, a proxy delegating to its implementation, is an internal call. Block explorers group these under an "Internal Txns" tab because that is how they read to a person, but in the data they are execution traces.
A single transaction can produce dozens of traces. A user calls a router; the router calls a pool; the pool calls a token contract; the token moves a balance; the pool sends ETH back. That is one transaction and five or more internal calls, arranged as a tree. The tree is where the real behavior of a transaction lives, which is why accounting tools, DeFi analytics, and investigators all end up needing it. For the investigation angle specifically, following funds through internal calls for transaction monitoring, the compliance data guide goes deeper; this one is about the data type itself and what it costs to obtain.
2. Why the transaction list misses them
The two data types most teams reach for first both leave internal calls out, for different reasons. The transaction list contains only top-level calls by definition, so anything inside a transaction is gone. Event logs exist only when a contract's code chooses to emit one, and most internal calls, along with every plain ETH forward, emit nothing.
The size of the gap is easy to measure. Take USDC on Ethereum over 20 blocks (21,000,000 to 21,000,020) and count it two ways. As the direct recipient of a transaction, the contract a user sent their transaction to, USDC appears 66 times. As the callee of a call, once internal calls are included, it appears 260 times. The 194-call difference is USDC being called from inside transactions aimed at something else: a DEX router, an aggregator, a multisig. A query that filters transactions by their to field cannot see any of it.
USDC on Ethereum, blocks 21,000,000-21,000,020
transactions where to = USDC ............ 66
calls where callTo = USDC ............... 260
----
internal calls a tx filter misses ....... 194 That ratio is not special to USDC. Any contract that other contracts call, which is most of the useful ones, is touched far more often from inside transactions than as a direct recipient. To see those touches you have to query the call tree itself.
3. The trace record
SQD's Portal exposes traces as a first-class data type. The query that finds every internal call into USDC, the 260 above, filters the trace stream by call type and callee:
POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream
Accept: application/x-ndjson
{
"type": "evm",
"fromBlock": 21000000,
"toBlock": 21000020,
"traces": [{
"type": ["call"],
"callTo": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]
}],
"fields": {
"trace": {
"type": true,
"traceAddress": true,
"callFrom": true,
"callTo": true,
"callValue": true,
"callCallType": true,
"error": true
}
}
}
Each trace carries its position in the call tree. traceAddress is [] for the top-level call, [0] for the first internal call, [0,0] for a call made inside that one, and so on, so the whole tree is reconstructable:
Transaction traceAddress []
|- call [0] router -> pool callValue 0
| |- call [0,0] pool -> token callValue 0
| \- call [0,1] pool -> recipient callValue X (ETH out)
\- call [1] router -> fee sink callValue Y
Two details to know before building on this. The response nests its data: the field requested as callTo arrives under action.to, callFrom under action.from, and the call type under action.callType, which separates an ordinary call from a delegatecall or a read-only staticcall. And traces carry a transactionIndex, not a transaction hash, so to tie a hop back to a hash you include the transaction in the query. With that, callValue on each hop is the ETH moved there, and the path of execution through a transaction is laid out rather than guessed.
4. Why traces cost so much from RPC
Logs and transactions are stored by every node as a normal part of keeping the chain, so reading them back is cheap. Traces are not stored. To produce them a node has to re-execute the transaction through an instrumented EVM that records every call as it goes. That is why debug_traceTransaction and trace_block are the heaviest methods a node exposes.
The practical consequences follow from that cost. Public and shared RPC endpoints commonly disable the tracing methods outright or rate-limit them hard, so they are often simply not available on the endpoint you have. Getting traces for historical blocks means running your own archive node with tracing enabled, which is the most demanding node configuration to operate. And because each call re-executes a transaction, pulling traces across a long range one transaction at a time is slow even when it is permitted. The result is that traces are usually the data type a project does not have, not because the queries are hard to write but because the source is hard to reach.
A read-side data layer changes the position. SQD does the re-execution and decoding once and serves the result as a queryable type, so the 20-block trace query above runs against an indexed stream rather than against a node's debug endpoint, and the same query works over a single block or the full history. The work that makes traces expensive does not go away; it stops being something every consumer has to pay for.
5. A second use: contract deployments
Following calls is the obvious use of traces, but the same data answers a different question cleanly: who deployed which contract, and when. A deployment is a create trace, a separate trace type, and filtering for it returns the deployer and the new contract address. This query scans a 10-block range for deployments:
{
"type": "evm",
"fromBlock": 21000000,
"toBlock": 21000010,
"traces": [{ "type": ["create"] }],
"fields": {
"block": { "number": true },
"trace": { "type": true, "createFrom": true, "createResultAddress": true, "transactionIndex": true }
}
} One real result, the first deployment in block 21,000,000, with the response shown in its nested form:
{
"type": "create",
"transactionIndex": 14,
"action": { "from": "0xcdc180b5dd8ede4dfbe212f2aefc8789d16089b8" },
"result": { "address": "0x521de88737bda991043ee9af429ec1fcabb87f23" }
}
The deployer is action.from and the contract it created is result.address. This is the definitive way to build a deployment registry, because it captures contracts made by factory contracts and by CREATE2, both of which look like ordinary calls in a transaction list rather than deployments. For DeFi work that means catching a new pool the moment its factory deploys it, from the trace stream, without watching for a hand-maintained address list.
6. Traces with SQD
The queries above are the actual interface. SQD provides traces as decoded, typed records from the Portal, alongside logs and transactions, so the internal calls a log-and-transaction feed misses are in scope rather than out of reach. The same query shape runs against every EVM dataset SQD indexes, changing only the network name in the endpoint, with full history from genesis.
For a production pipeline you would stream traces rather than hand-write requests. The Squid and Pipes SDKs pull traces into your own store with the same call and create filters, where they become internal-transfer tables, swap-path records, or a deployment registry that updates as blocks arrive. For following funds through traces in a monitoring context, see the compliance data guide; for how trace-level data feeds trading and DeFi products, see the DeFi and trading solution page.
Frequently asked questions
What is an internal transaction?
Why do internal transactions not show up in the logs or the transaction list?
How do I query internal transactions (traces)?
Why are traces expensive to get from an RPC node?
Can I find which contract a deployer created from traces?
Related guides
Need internal calls and deployments at scale?
See how trace-level data feeds DeFi and trading products on the DeFi and trading solution page.