blockchainsv
Web3 Integration & APIs·August 19, 2026·18 min read

Subgraphs vs RPC queries: choosing a dapp data strategy

A wallet dashboard can display the current native ETH balance with a single eth_getBalance request. An ERC-20 balance requires a contract read, usually an eth_call invoking balanceOf. Both responses may arrive quickly.

Subgraphs vs RPC queries: choosing a dapp data strategy

But the moment a user opens Activity to see the last hundred swaps, the architecture changes: the frontend starts querying logs across block ranges, decoding events, filtering addresses, and assembling records that the chain never stored as a ready-made list.

That is when a simple read path turns into a data problem.

The data your dapp needs lives on-chain, but blockchains do not store information the way a relational database does. They record transactions chronologically, append-only, and the moment you need anything more sophisticated than the current state of one contract, you are working against that shape. Sooner or later, every team reaches the same fork in the road: keep going with raw RPC calls, or invest in indexing.

The right answer is rarely one technology everywhere. Direct RPC and subgraphs solve different classes of questions. The useful comparison is not simply “The Graph subgraph vs RPC query,” but which layer should answer which question, with what freshness guarantees, operational cost, and tolerance for complexity.

The Mechanics of Raw Data Retrieval: How JSON-RPC Queries Function

Direct JSON-RPC is the most immediate interface to a blockchain. You are asking an EVM node—your own or a provider’s—to read state, return logs, estimate execution, submit a transaction, or report information about the chain head.

Methods such as eth_getBalance, eth_call, eth_blockNumber, and eth_getLogs sit underneath most Web3 applications. Libraries such as ethers.js and viem make these requests more convenient, but they do not change the underlying model.

The distinction between these methods matters:

  • eth_getBalance reads the native coin balance of an address at a specified block or at the latest available state. For Ethereum, that means ETH; on another EVM chain, it means that chain’s native asset.
  • eth_call executes a contract method locally against a node’s state without creating a transaction. It is the usual way to read an ERC-20 balanceOf, inspect an NFT owner, retrieve a protocol parameter, or call any other view or pure function.
  • eth_getLogs searches emitted event logs over a block range, subject to the node’s supported filters and provider limits.
  • eth_blockNumber tells the application how far the node has progressed and is often used to coordinate polling or compare an RPC response with an indexed data source.

A wallet view that displays both ETH and token balances therefore uses different RPC mechanisms. The native ETH balance comes from eth_getBalance. Each ERC-20 balance is generally an eth_call to the token contract’s balanceOf function. That may still be a small number of requests, but treating every balance as an eth_call obscures an important architectural boundary: native account state is not stored in an ERC-20 contract.

The strength of raw RPC is immediacy. There is no separate indexing pipeline, no schema translation, and no additional data model between the application and the chain. When you request an ERC-20 balance with eth_call, the node evaluates the contract read against its current state. When you request a native ETH balance with eth_getBalance, the node reads the account state directly. For current, single-contract data, this is difficult to beat for simplicity and freshness.

The friction appears when the query becomes historical, relational, or highly filtered.

Suppose the interface needs to show a user’s trade history. The chain does not expose a getTradesByUser method. Your application must locate the relevant swap events, usually by scanning logs across a block range, filtering by contract address and event topics, decoding the ABI, and then sorting or grouping the results. If the user wants a second page, the frontend needs a reliable cursor or another block-range strategy. If the application supports several protocols, each protocol may emit different event shapes and require separate decoders.

The same problem appears in analytics. To calculate total volume for a Uniswap-style pair over a historical period, the application has to retrieve the relevant events, interpret token amounts and decimals, account for the correct pool, and aggregate the results. Each individual eth_getLogs call may be reasonable. Together, the calls create a pipeline of pagination, retries, rate-limit handling, decoding, deduplication, and reorganization handling.

This is not a flaw in the protocol. It is what an append-only ledger looks like when queried as if it were an application database.

Direct RPC is excellent at “what is the state right now” and much less convenient at “tell me everything that ever happened.”

There is also a practical limit to what an RPC provider will allow. Providers commonly impose maximum block ranges for log queries, request quotas, response-size limits, and concurrency limits. A query that works against a local node may fail against a hosted endpoint. A query that works for one user may become expensive when thousands of users trigger it at the same time.

Raw RPC remains the source of truth, but source of truth does not mean the best interface for every product feature.

Indexing for Performance: The Role of Subgraphs in Complex Data Aggregation

A subgraph is a purpose-built indexed view of blockchain activity. You define the contracts and events to watch, describe the entities your application needs, and provide mappings that transform on-chain activity into structured records. A Graph Node then processes the chain, runs those mappings, and exposes the resulting data through GraphQL.

The shift is fundamental. Instead of asking the chain to search itself every time a user opens a page, you perform the expensive interpretation once during indexing and serve the prepared result repeatedly.

A typical subgraph includes three related pieces:

1. A manifest, commonly represented by subgraph.yaml, identifies the data sources, contract addresses, event handlers, ABIs, and starting blocks.

2. A GraphQL schema, commonly represented by schema.graphql, defines entities such as User, Pool, Position, or Trade, along with their fields and relationships.

3. Mapping code, often written in AssemblyScript, reacts to events and writes or updates those entities.

Consider a Swap event. A mapping can decode the trader, token amounts, pool address, transaction information, and block timestamp. It can then create a Trade entity and update a corresponding Pool entity. The frontend no longer needs to reconstruct those relationships from raw logs. It can request the latest trades for a pool, order them, filter them by a wallet, and paginate through the result using the schema exposed by the subgraph.

The heavy lifting—block-by-block scanning, event decoding, normalization, and many application-specific relationships—happens during indexing rather than in every user’s browser.

That does not make a subgraph a replica of the blockchain. It is a derived data product, and the distinction should remain visible in the architecture. A subgraph can have indexing lag. It can contain a mapping bug. It can require a schema migration. It can temporarily fall behind the chain or fail to index a particular block. The result is useful precisely because it is specialized, not because it eliminates the underlying complexity.

A subgraph also cannot infer data that the contract never emitted or exposed. If a protocol stores a value internally without an event and without a readable contract method, an indexer has no reliable way to reconstruct every historical change. Good subgraph design begins with the protocol’s event model, not with the desired GraphQL query.

The cost is therefore maintenance. Mappings must evolve when contracts change. Deployments across multiple networks need separate data sources and careful start blocks. Reorg behavior, entity IDs, missing fields, and derived values need deliberate handling. But for features that require aggregated, filtered, or paginated historical data, maintaining that pipeline is usually more predictable than repeating the same reconstruction work in every client.

Architectural Trade-offs: Latency, Historical Depth, and Node Requirements

The choice between direct RPC and subgraphs becomes clearer when the trade-offs are made explicit.

DimensionDirect JSON-RPCSubgraphs and GraphQL
Best forCurrent state, transaction submission, and focused contract readsHistorical, aggregated, filtered, and multi-contract data
Native balance readseth_getBalance against the account statePossible after indexing, but usually unnecessary for a simple live balance
Contract readseth_call for methods such as ERC-20 balanceOf and other view functionsCan expose indexed results, but may lag behind the latest state
Event historyeth_getLogs with client-managed block ranges and decodingPre-indexed entities and fields queried through GraphQL
FreshnessDirectly reflects the queried node’s latest stateDepends on indexing progress and finality strategy
Historical depthDepends on node access, provider limits, and the requested rangeDepends on the subgraph’s start block, retained data, and indexing configuration
Client complexityPagination, ABI decoding, retries, aggregation, and deduplication remain in application codeMost query assembly and historical aggregation move into mappings and the index
InfrastructureA dependable RPC endpoint, often with a fallback providerA hosted or self-hosted indexer plus EVM JSON-RPC access
Cost shapeRequest volume and response size drive RPC usageIndexing resources and query usage drive the cost
Typical failure modeRate limits, provider errors, incomplete scans, and inconsistent paginationMapping bugs, schema changes, indexing lag, and failed deployments

Latency needs a more careful definition than “which one is faster.” A single eth_call or eth_getBalance is usually faster than waiting for an indexer to process a new block. A historical query assembled from hundreds of log requests is a different matter. A subgraph can return the prepared result quickly because the scan already happened.

This leads to a common hybrid design:

  • Use RPC for live state that must reflect the latest block.
  • Use a subgraph for historical records and application-specific aggregations.
  • Use transaction receipts and RPC polling for submitted transactions.
  • Present a clear loading or synchronization state when indexed data has not caught up.

One important detail is often missed: a Graph Node is not magic. It still needs access to an EVM-compatible JSON-RPC endpoint under the hood. Depending on the data sources and handlers in use, the node may require capabilities beyond basic contract reads and log retrieval. Some deployments rely on richer block references or tracing-related methods. If an application uses call handlers or other advanced indexing behavior, the upstream RPC must support the calls that the Graph Node expects.

Subgraphs replace client-side historical-query complexity. They do not remove the need for a reliable chain connection.

Historical depth has its own trap. A direct RPC query is not automatically capable of searching the entire chain. The provider may restrict the block range, retain only certain data, or make broad scans impractical. A subgraph also does not automatically contain the entire chain: its data begins at the configured start block and reflects only the contracts, events, and mappings defined in the deployment.

The question is not “does this option support history?” Both can, in different ways. The question is who pays for discovering and organizing that history: every client request, or an indexing pipeline maintained by the application.

Strategic Implementation: When to Prioritize Direct RPC Calls

There are entire categories of dapp interactions where reaching for a subgraph is unnecessary.

Wallet connections and balance displays are the obvious examples, but the implementation should separate native and token balances. To show a connected user’s native ETH balance, call eth_getBalance for the wallet address. To show ERC-20 balances, make eth_call requests to the relevant token contracts, invoking balanceOf for that address. For a small, known set of tokens, this is straightforward and keeps the data as fresh as the RPC node’s latest state.

The same principle applies to current ownership and contract configuration. A dapp may need to read the owner of an NFT, check a user’s allowance, retrieve a protocol’s current fee, or determine whether a wallet is eligible according to a view function. These are present-tense questions. A subgraph may eventually contain an approximation or a derived record, but using an indexer as the primary source can introduce lag where none is needed.

Transaction submission is RPC territory as well. There is no indexed substitute for eth_sendRawTransaction. Wallet signatures, nonce management, gas estimation, receipt polling, replacement transactions, and mempool-aware status handling all belong to the transaction path. A subgraph can help display a transaction after it has been indexed, but it cannot submit the transaction or guarantee that a pending transaction will be included.

Small event lookups can also stay on RPC. If the application checks a narrow block range, watches a single contract, or retrieves the receipt for a transaction it already knows about, direct methods are usually easier to reason about. A notification that asks whether a particular event was emitted by a known transaction does not justify building a full historical index.

A useful heuristic is to ask what the query knows before it starts:

  • If it knows the contract, method, and current account, use a contract read.
  • If it knows the transaction hash, use receipt and transaction methods.
  • If it needs a current native balance, use eth_getBalance.
  • If it needs a current token balance or another contract-state value, use eth_call.
  • If it needs a narrow event range, use eth_getLogs with explicit pagination and limits.
  • If it needs an open-ended list, joins, rankings, or repeated historical aggregation, consider indexing.

This approach also reduces unnecessary infrastructure. A subgraph introduces deployment, monitoring, schema management, and a second data freshness model. Those costs are justified when they remove a larger recurring burden, not when they merely duplicate a simple RPC read.

Scaling Dapp Infrastructure: Leveraging The Graph Network and Decentralized Indexing

The moment a UI starts looking like a data product—leaderboards, historical charts, search filters, paginated tables, portfolio timelines—the limits of raw RPC become much more visible.

Three patterns are especially strong candidates for indexing.

The first is cross-contract data. A lending dashboard that aggregates positions across Aave, Compound, and several vaults needs to combine events from multiple deployments and normalize them into a coherent view. With raw RPC, the client or an intermediary service must coordinate multiple eth_getLogs scans, decode different ABIs, reconcile timestamps and token units, and stitch the results together. A subgraph can watch those contracts, write normalized entities into one schema, and expose relationships through a single query surface.

The second is analytics and time series. Total volume, liquidity changes, top traders, utilization, and daily activity all depend on historical events. The application may need to group records by day, calculate derived values, and preserve enough context to serve the same chart repeatedly. Doing this at request time is wasteful. Indexing turns the historical scan into a maintained data pipeline.

The third is a user-facing search experience. An NFT marketplace that filters listings by collection, traits, price, seller, and status is not asking for one contract read. It is asking for an application database backed by blockchain events. Raw RPC can supply the underlying data, but making it useful requires an indexing layer somewhere.

This is where The Graph’s broader network becomes relevant. Beyond self-hosted Graph Nodes, The Graph Network is a decentralized marketplace in which Indexers operate infrastructure, Curators signal which subgraphs deserve indexing attention through GRT, and Delegators support Indexers. The network introduces an economic coordination layer around serving indexed blockchain data rather than requiring every application team to operate its own complete indexing stack.

The curation mechanism is interesting because it connects technical availability with demand. A subgraph is not valuable merely because it exists; it needs to be indexed and served reliably enough for applications to depend on it. The same attention to curation and quality that shapes any creator economy, from music platforms to the top creator agencies redefining influencer marketing strategy, appears here in a different form: participants signal which data products deserve resources, while Indexers decide which services they can operate profitably.

For teams that do not want to run infrastructure, a hosted service or a gateway to the decentralized network can reduce the operational burden. The team still owns the schema and mapping logic, but it does not necessarily need to provision database storage, maintain a Graph Node, monitor synchronization, and handle every query-serving concern directly.

That convenience should not be confused with operational invisibility. A production integration still needs to monitor indexing status, validate returned data, track schema versions, and define a fallback for important user flows. A decentralized data source can improve availability and choice, but it does not absolve the application from deciding which data is authoritative for each feature.

Practical Patterns for a Hybrid Data Layer

Most mature dapps do not make a single irreversible choice between RPC and subgraphs. They assign each source a role and make the boundary explicit.

Run RPC and subgraphs side by side at first. Keep RPC for balances, ownership checks, contract reads, transaction submission, and receipt status. Add the subgraph for historical activity, cross-contract views, and complex filters. This lets the team compare results before making the indexed path responsible for a major part of the user experience.

Treat freshness as part of the product, not merely an infrastructure detail. A user who submits a swap may see the transaction in the wallet and the receipt through RPC before the corresponding trade appears in the subgraph. That is normal. The interface can show the pending transaction from the RPC path, then merge or reconcile it with indexed history once the indexer catches up.

Reorganizations require the same care. An event observed in a non-final block may disappear or be replaced. A UI that combines live RPC data with indexed history should avoid presenting both records as independent facts. Stable identifiers, transaction hashes, log indexes, and explicit confirmation states help prevent duplicate or phantom activity.

Schema design deserves the same discipline as API design. GraphQL schemas are easy to start and surprisingly expensive to change once several clients depend on them. Entity IDs should be deterministic. Token addresses, pool addresses, and network identifiers need a consistent representation. Fields that may not exist for every event should be nullable rather than forced into misleading defaults. Renames, type changes, and removals should be handled through deprecation and controlled client migration.

Mappings should keep derived values explainable. If a Trade entity stores a calculated price, volume, or fee, the application should know which event fields produced it and which decimals or exchange-rate assumptions were applied. The indexer is not just copying logs; it is creating an interpretation of them. That interpretation needs tests.

A practical set of implementation habits looks like this:

  • Define the source of truth per field. The latest native balance may come from eth_getBalance; an activity list may come from a subgraph; transaction status may come from the receipt path.
  • Add cursors and bounded ranges to RPC scans. Never assume that a provider will accept an unbounded eth_getLogs request.
  • Make indexing lag observable. Track the latest indexed block and compare it with the chain head exposed by RPC.
  • Keep a recovery path for critical pages. If the subgraph is behind, the application may show a temporary state or retrieve narrowly scoped recent events through RPC.
  • Test mappings against representative forked or historical data. A mapping that compiles can still assign the wrong entity ID, mishandle a token decimal, or miss an event variant.
  • Verify provider capabilities before deploying a Graph Node. Unsupported tracing or call-related methods can look like mapping failures when the actual problem is the upstream RPC endpoint.
  • Reconcile data after upgrades. Contract migrations, proxy implementations, new event versions, and redeployments often require multiple data sources or explicit mapping branches.

The key is to avoid pretending that a subgraph is a faster RPC endpoint. It is a different read model with different guarantees.

Making the Decision Without Painting the Architecture Into a Corner

The practical answer to “subgraphs or RPC?” is almost always “both, on different layers.” They are not competing technologies. JSON-RPC is the live connection to the chain: current state, contract execution, logs, transaction submission, and the substrate on which other data services depend. A subgraph is a read-optimized view: precomputed, indexed, and shaped around the questions users ask repeatedly.

RPC gives you the chain as it is. A subgraph gives you the chain organized for a product.

If a feature needs the present tense of the chain—native balances, token balanceOf reads, allowances, nonces, current ownership, gas estimates, or transaction receipts—it usually belongs in the RPC layer. Use eth_getBalance for native ETH, eth_call for ERC-20 reads and other contract-state queries, and eth_getLogs for bounded event retrieval.

If a feature needs the past tense of the chain—history, analytics, cross-contract aggregation, rankings, searchable entities, or large paginated lists—it usually belongs in an indexed layer. A subgraph can make that history queryable without forcing every browser session to replay the same logs.

The architectural mistake is not choosing RPC over subgraphs or subgraphs over RPC. It is asking one of them to behave like the other. Raw RPC should not be treated as a relational database, and a subgraph should not be treated as an instantaneous mirror of the latest block.

Build the integration around the question being asked. For current state, read directly. For repeated historical interpretation, index once and query many times. The user is waiting either way; the goal is to make sure the architecture is doing the waiting and the work in the right place.

FAQ

When should I use direct RPC instead of a subgraph?
Use direct RPC for present-tense queries such as checking native ETH balances, reading ERC-20 token balances via contract calls, verifying current ownership, or submitting transactions.
Why can't I just use RPC for historical data?
While possible, using RPC for history requires the application to manually scan logs, decode events, and aggregate data for every user request, which is inefficient and often limited by provider rate limits and block range restrictions.
Does a subgraph provide real-time data?
A subgraph is a derived data product and may experience indexing lag, meaning it might not always reflect the absolute latest state of the blockchain compared to a direct RPC call.
What are the main maintenance costs of using subgraphs?
Subgraphs require managing schemas, writing and updating mapping code as contracts evolve, handling indexing lag, and ensuring the indexer remains synchronized with the chain.
Can a subgraph replace my need for an RPC provider?
No, a subgraph is not a standalone replacement for an RPC connection. A Graph Node requires an underlying EVM-compatible JSON-RPC endpoint to access the blockchain and process data.

By Chloe Redfern