Compare The Graph subgraphs and direct RPC queries
Forty thousand rows. That was the size of the transfer-history payload the dApp needed to render its Q3 leaderboard. The team pulled it with a single eth_getLogs call spanning ninety days. The provider returned 8,200 rows and a 10,000-block range error.

Forty thousand rows. That was the size of the transfer-history payload the dApp needed to render its Q3 leaderboard. The team pulled it with a single eth_getLogs call spanning ninety days. The provider returned 8,200 rows and a 10,000-block range error. The chart was empty for four hours. The fix was a subgraph.
This is the operational cost of picking the wrong data layer. The decision to compare The Graph subgraphs and direct RPC queries is not stylistic. It sets the latency budget, the query expressiveness, and the failure modes of every surface in the frontend. Teams pick late. They pay in middleware.
The architectural split: indexed graph vs. raw node access
The Graph is a decentralized indexing protocol. A subgraph manifest declares which contracts to watch, which events to capture, and how to map them into entities. Handlers run in a Graph Node. The result lands in a GraphQL-accessible store. Entities have relations. Filtering is a server-side clause. The shape is fixed at indexing time.
For related context, see DeFi yield farming, staking and passive income.
Direct RPC queries hit an Ethereum node. eth_call reads contract storage at the head the node has synced to. eth_getLogs scans event logs across a block range. The response is whatever the node serialized. There is no schema. There is no relation layer. There is the chain, as the node sees it.
The two paths answer different questions. Subgraphs answer: what is the set of all transfers from address X to address Y, joined with token metadata, sorted by USD value, over the last thirty days. RPC answers: what is the balance of address X right now.
Indexed data is a derived view of the chain. Raw RPC is the chain itself. Conflating the two breaks the consistency invariant the frontend depends on.
The distinction is not about preference. It is about which questions can be answered in a single round trip.
Latency vs. immediacy: where each path wins
Subgraphs have indexing latency. A transaction confirmed at block N appears in the subgraph after several blocks of catchup. The exact delay depends on the indexer, the subgraph complexity, and network conditions. Seconds to minutes is the normal range on Ethereum mainnet. Longer under load. Subgraphs on L2 networks tend to sync faster, but the architecture is the same.
RPC returns the node's current view. There is no catchup window. There is no derived index. A balance query against a synced node returns the balance at the head the node has processed. For a healthy full node, this is one block deep — typically twelve seconds on mainnet, two seconds on most L2s.
The trade is determinism against immediacy. RPC gives the present. Subgraphs give a coherent history, eventually. A wallet balance widget needs the present. An analytics page needs the history. Mixing the requirements into one query path is the root cause of most "stale data" tickets in production.
Concrete failure mode: a frontend polls a subgraph for the latest trade on a bonding curve. The transaction confirmed two blocks ago. The subgraph has not caught up. The user sees a quote based on a state that no longer exists. They trade on stale data. The protocol absorbs a sandwich.
The fix is not a faster subgraph. The fix is routing real-time reads through RPC and historical reads through The Graph.
The eth_getLogs ceiling
Direct RPC has hard limits. eth_getLogs is constrained by:
- Block range per request. Most hosted providers cap this at 2,000 to 10,000 blocks per call. Alchemy, Infura, and QuickNode enforce these caps at the gateway. Self-hosted nodes can be configured higher, at the cost of memory and CPU on the scan.
- Compute per request. Even within the range, returning tens of thousands of matched logs with full indexed data exceeds node resource budgets. The request fails or truncates silently.
- Historical depth. Scanning the full history of an active ERC-20 means iterating ranges. Hundreds of RPC calls. Each call carries its own latency and rate-limit budget.
Subgraphs have no such ceiling. The indexer processed the entire history at deploy time, or during sync after a reorg. Querying a subgraph returns paginated, filtered results from a Postgres-backed store. The cost is bounded. The shape is consistent.
Block range limits are not a soft suggestion. They are enforced at the provider layer, often without a clean error.
Self-hosting a node gives range-cap control at the cost of operations: state growth, pruning, archive mode for older blocks, monitoring. The teams that self-host are typically indexers or analytics platforms, not application frontends.
Time travel and relational queries
The Graph supports Time Travel natively. A developer queries the subgraph with a block-height argument. The response is the entity set as it existed at that height, reconstructed from the indexed event history. The query is a single GraphQL call. It is fast.
RPC can simulate past state. eth_call accepts a block tag, including historical numbers. It returns the contract state at that block. This works for single-contract reads. It does not scale to relational queries: give me all votes in proposal 42, joined with voter delegation history, filtered by treasury size at vote time.
The chain does not store those joins. They have to be reconstructed. Per request. By the client. Across multiple contract calls. With pagination. With error handling for any individual call that fails.
Subgraphs encode relations at index time. Joins are GraphQL clauses. Filtering is server-side. The client receives a single response. The frontend renders. The user does not wait.
A second consideration is reorg handling. RPC against a single node can return a state that a reorg later invalidates. Subgraphs built on finalized-block semantics can offer stronger consistency guarantees, at the cost of additional latency. The trade is explicit. The choice belongs to the developer.
Picking the right tool for the dApp layer
| Parameter | Direct RPC | The Graph Subgraph |
|---|---|---|
| Query language | JSON-RPC | GraphQL |
| Data shape | Raw chain state | Structured entities |
| Real-time accuracy | Immediate at node head | Delayed by indexing latency |
| Historical depth | Limited by range caps | Full history at sync time |
| Relational queries | Client-side reconstruction | Server-side joins |
| Time Travel | Per-call block tag | Native subgraph support |
| Compute cost | Per request, node-bound | Per query, subgraph-bound |
| Failure mode | Truncation, rate limits, reorg drift | Stale data, sync gaps, reorg recovery |
| Operational burden | Node hosting or vendor SLA | Subgraph deployment, indexing fees |
Use direct RPC when:
- The surface needs the current state of a single contract at the head block.
- The data point is one read, one response.
- The latency budget is sub-second and no derived data is acceptable.
- The query is a transaction submission check or a wallet balance display.
Use a subgraph when:
- The surface needs filtered, sorted, paginated event history.
- The data crosses contracts and requires joins.
- The surface is a dashboard, leaderboard, analytics page, or any view that aggregates over time.
- Time Travel is required for governance, vesting, or historical state reconstruction.
Hybrid is the default for production frontends. A wallet widget reads through RPC. A historical view reads through The Graph. The boundary should be explicit in the architecture document, not an accident of which library the first developer reached for.
Hosted infrastructure is its own decision axis. Self-hosting a node gives range-cap control at the cost of ops. Hosted RPC providers give SLAs at the cost of vendor limits. Subgraph services — The Graph's decentralized network, hosted providers like Goldsky and SubQuery — give managed indexing at the cost of vendor dependency and indexing fees. For a vendor-neutral survey of digital tooling and infrastructure providers in adjacent categories, third-party infrastructure reviews cover the trade space in terms that survive marketing copy.
Architectural checklist
Before shipping the data layer, verify:
1. Latency budget per surface. Wallet UIs: sub-second. Dashboards: seconds acceptable. Document each.
2. Query shape per surface. Single read: RPC. Set, join, filter, paginate: subgraph. No surface should mix the two requirements.
3. Failure mode per surface. RPC truncates under load and rate-limits at the provider. Subgraphs go stale during reorg recovery. Both have known failure shapes. Document them.
4. Block range needs per surface. Under 10,000 blocks: RPC viable. Over: subgraph required. Calculate against provider caps, not against ideal node behavior.
5. Relational depth per surface. Zero joins: RPC. Any joins: subgraph. Cost the client-side reconstruction before accepting it.
6. Consistency model per surface. RPC against a single node is one-node-consistent. Subgraphs against finalized blocks are chain-consistent at reorg depth. Pick the model that matches the surface.
7. Vendor boundaries. One hosted RPC, one hosted subgraph, one self-hosted fallback. Document the failover path before the first outage.
Both layers stay in a production Web3 stack. RPC handles state. Subgraphs handle history. The boundary is an architectural decision, not a per-feature compromise.
Pick it before the user does.
FAQ
When should I use direct RPC instead of a subgraph?
Why do my eth_getLogs queries fail or return incomplete data?
What is the main disadvantage of using The Graph for real-time data?
Can I perform relational queries using direct RPC?
How does Time Travel work in The Graph?
By Caleb North