blockchainsv
Web3 Integration & APIs·June 28, 2026·8 min read

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.

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. 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

ParameterDirect RPCThe Graph Subgraph
Query languageJSON-RPCGraphQL
Data shapeRaw chain stateStructured entities
Real-time accuracyImmediate at node headDelayed by indexing latency
Historical depthLimited by range capsFull history at sync time
Relational queriesClient-side reconstructionServer-side joins
Time TravelPer-call block tagNative subgraph support
Compute costPer request, node-boundPer query, subgraph-bound
Failure modeTruncation, rate limits, reorg driftStale data, sync gaps, reorg recovery
Operational burdenNode hosting or vendor SLASubgraph 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?
Use direct RPC when you need the current state of a single contract at the head block, require sub-second latency, or are performing simple tasks like checking a wallet balance.
Why do my eth_getLogs queries fail or return incomplete data?
Most RPC providers enforce strict block range caps, typically between 2,000 and 10,000 blocks, and limit the compute resources available for a single request.
What is the main disadvantage of using The Graph for real-time data?
Subgraphs experience indexing latency, meaning there is a delay between a transaction being confirmed on-chain and it appearing in the subgraph, which can lead to stale data in the frontend.
Can I perform relational queries using direct RPC?
Direct RPC does not support server-side joins; you would have to reconstruct relational data on the client side across multiple contract calls, which is inefficient and difficult to scale.
How does Time Travel work in The Graph?
The Graph supports Time Travel natively by allowing developers to query a subgraph with a specific block-height argument to retrieve the entity set as it existed at that point in history.

By Caleb North