Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides11 min read

The Graph Protocol: Indexing Blockchain Data with Subgraphs

A practical guide to The Graph protocol: how subgraphs index on-chain data, how to deploy and query them, and how to verify indexing health.

TL;DR

The Graph protocol is a decentralized indexing layer that turns raw blockchain events into queryable GraphQL APIs called subgraphs. Developers define a manifest, schema, and mapping handlers; Graph Node scans blocks through an RPC endpoint, executes those handlers, and stores the resulting entities for fast queries. This article explains the indexing mechanism, walks through a runnable subgraph deployment and query workflow, and shows how to verify results and troubleshoot stalled or inconsistent indexing. It separates documented protocol behavior from the measurements you should run yourself against your own RPC infrastructure.

What The Graph protocol does and where it fits

The Graph protocol is an open indexing protocol for blockchain data. Instead of querying raw logs and receipts from an RPC node on every page load, you describe the data you care about in a subgraph, and the protocol's indexers process blocks into a database that answers GraphQL queries. The public description is consistent across the project's documentation and ecosystem listings: subgraphs organize smart contract data and serve it over GraphQL, while Substreams cover streaming pipelines for higher-throughput use cases.

The practical mental model is a three-part pipeline. A manifest declares which contracts, events, and start blocks to watch. A schema declares the entities you want to store. Mapping handlers are deterministic functions that transform each matching event into entity writes. Graph Node is the open-source Rust implementation that runs this pipeline against a chain RPC endpoint.

This article focuses on the protocol mechanics and a reproducible local workflow. For the concrete choice of an RPC endpoint that Graph Node will use, see the endpoint recommendations in the RPC Assistant rather than treating this page as a provider list.

  • Subgraphs: declarative indexing of contract events into GraphQL entities.
  • Substreams: streaming data pipelines for high-volume or real-time consumers.
  • Graph Node: the indexer process that reads blocks and writes the entity store.
  • GraphQL endpoint: the query surface your application calls.

Assumptions and prerequisites

The workflow below assumes you have Node.js and npm installed, Docker available for running Graph Node locally, and an archive-capable RPC endpoint for the chain you are indexing. Archive access matters because a subgraph with a historical start block must replay past state; a pruned endpoint will fail or stall during backfill.

You also need a contract address and the ABI for the events you want to index. Keep the ABI minimal: only the events and functions your mappings call need to be present. If you are choosing between self-hosted and managed RPC for the indexer's data source, the dedicated node options and the RPC endpoint guide explain the operational tradeoffs.

How indexing works inside Graph Node

Graph Node processes the chain in block order. For each block it checks the manifest's data sources, filters logs by contract address and event signature, and invokes the matching handler. Handlers receive the event and a store handle; they read or create entities and save them. Because handlers must be deterministic, the same block range always produces the same entity state, which is what allows independent indexers to agree.

Two failure modes dominate real deployments. First, an RPC endpoint that cannot serve the required block range or rate-limits the indexer will cause retries and visible lag. Second, a handler that depends on non-deterministic input, such as wall-clock time or an external HTTP call, will produce divergent results and is rejected by the protocol's determinism rules. Treat the RPC layer as part of your indexing correctness surface, not just plumbing.

The protocol's decentralized network adds economic roles on top of this mechanism: indexers stake and serve queries, curators signal which subgraphs matter, and delegators back indexers. Those roles affect query economics and availability, but they do not change the subgraph authoring workflow described here.

Building a minimal subgraph manifest and schema

Start with the standard Graph CLI scaffolding, then replace the generated files with your contract details. The manifest below is a template; substitute your contract address, start block, and ABI path. The startBlock should be the block where the contract was deployed unless you intentionally need earlier history.

npm install -g @graphprotocol/graph-cli
graph init my-subgraph \
  --from-contract 0xYourContractAddress \
  --network mainnet \
  --abi ./abis/MyContract.json \
  --contract-name MyContract

Writing deterministic mapping handlers

The generated subgraph.yaml points at a mapping file. A handler should do three things: read the event parameters, load or create the entity, and save it. Avoid any call that can return different results on different machines. If you need derived values, compute them from event data or from entities already in the store.

The example below indexes a generic Transfer event into a Transfer entity and updates a running balance entity. It is intentionally small so you can verify the pipeline end to end before adding complexity.

import { Transfer as TransferEvent } from '../generated/MyContract/MyContract';
import { Transfer, Account } from '../generated/schema';
import { BigInt } from '@graphprotocol/graph-ts';

export function handleTransfer(event: TransferEvent): void {
  const id = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
  const transfer = new Transfer(id);
  transfer.from = event.params.from;
  transfer.to = event.params.to;
  transfer.value = event.params.value;
  transfer.blockNumber = event.block.number;
  transfer.save();

  const accountId = event.params.to.toHex();
  let account = Account.load(accountId);
  if (account == null) {
    account = new Account(accountId);
    account.balance = BigInt.fromI32(0);
  }
  account.balance = account.balance.plus(event.params.value);
  account.save();
}

Running Graph Node locally against an RPC endpoint

A local Graph Node instance needs a Postgres database, an IPFS node for subgraph files, and an RPC URL for the target chain. The compose file below is a starting point; set RPC_URL to an endpoint that supports archive queries for your chain. Do not hardcode a public endpoint into a shared repository, because rate limits and availability vary by provider and plan.

After the stack is healthy, create the subgraph, deploy it locally, and wait for the indexer to reach the chain head. The graph deploy command prints a status URL you can poll.

version: '3'
services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: graph
      POSTGRES_PASSWORD: graph
      POSTGRES_DB: graph-node
    ports: ['5432:5432']
  ipfs:
    image: ipfs/go-ipfs:v0.18.1
    ports: ['5001:5001']
  graph-node:
    image: graphprotocol/graph-node
    depends_on: [postgres, ipfs]
    ports: ['8000:8000', '8020:8020', '8030:8030']
    environment:
      postgres_host: postgres
      postgres_user: graph
      postgres_pass: graph
      postgres_db: graph-node
      ipfs: 'ipfs:5001'
      ethereum: 'mainnet:${RPC_URL}'
      GRAPH_LOG: info

Querying the subgraph over GraphQL

Once indexing reaches the head, query the local GraphQL endpoint. The query below returns the ten largest transfers by value and the current balance of a specific account. Use variables rather than string interpolation so the query stays cacheable and safe.

The expected verification result is a JSON response with a data object containing the requested entities. If data is null, read the errors array first; it usually names the missing field or entity type.

curl -s http://localhost:8000/subgraphs/name/my-subgraph \
  -H 'content-type: application/json' \
  --data '{"query":"query($account: String!) { transfers(first: 10, orderBy: value, orderDirection: desc) { id from to value blockNumber } account(id: $account) { id balance } }","variables":{"account":"0x0000000000000000000000000000000000000000"}}'

Verifying indexing health and measuring query latency

Do not trust a subgraph just because it returns rows. Verify three things: the indexer has reached the chain head, the entity counts match an independent source for a sample block range, and query latency is acceptable for your application. The procedure below produces a results table you fill with your own measurements; no numbers are asserted here because they depend on your chain, endpoint, and hardware.

Run each step at least three times and record the median. Compare against a direct RPC eth_blockNumber call to confirm the indexer is not lagging.

  • Step 1: call eth_blockNumber on your RPC URL and record the head block.
  • Step 2: query the subgraph's _meta { block { number } } field and record the indexed block.
  • Step 3: compute lag as head minus indexed block; repeat after 60 seconds.
  • Step 4: time ten identical GraphQL queries and record median latency.
  • Step 5: spot-check one entity against the raw event in a block explorer.

Results table template for your own runs

RunRPC head blockIndexed blockLag (blocks)Median query latency (ms)Entity spot-check
1fill from eth_blockNumberfill from _metacomputedmeasuredpass/fail
2fill from eth_blockNumberfill from _metacomputedmeasuredpass/fail
3fill from eth_blockNumberfill from _metacomputedmeasuredpass/fail

A stable lag near zero and a passing spot-check indicate a healthy pipeline. Rising lag with a healthy RPC usually points to handler inefficiency or database contention. Rising lag with RPC errors points to the endpoint, not the subgraph.

Troubleshooting stalled indexing and inconsistent entities

If indexing stops advancing, check the Graph Node logs for RPC errors before changing the subgraph. Common causes are a pruned endpoint that cannot serve historical blocks, rate limiting during backfill, or a chain reorg that forces the indexer to rewind. Fix the data source first, then re-run.

If entities are missing or duplicated, inspect the handler's entity ID construction. IDs built only from transaction hash collide when a transaction emits multiple matching events; append the log index as in the example above. If a handler reads an entity that a later handler in the same block creates, reorder the handlers or restructure the schema so writes are independent.

For chains with frequent reorgs, confirm your endpoint's finality assumptions match the subgraph's configured confirmation requirements. The network directory lists supported chains, and the reliability guidance covers endpoint selection criteria.

Limitations, tradeoffs, and next steps

Subgraphs are excellent for entity-shaped reads, but they are not a general-purpose streaming bus. If you need sub-second delivery of every event into a custom sink, evaluate Substreams or a direct log subscription instead. Indexing also adds latency between chain head and queryable state, so applications that must read the absolute latest block should fall back to an RPC call for that specific read.

Operationally, the RPC endpoint behind Graph Node is a first-class dependency. Budget for archive access, rate limits that accommodate backfill, and monitoring that alerts on indexing lag. For cost and plan comparisons, see the RPC pricing page.

As a next step, deploy the same subgraph to a testnet, run the verification table again, and compare lag and latency across endpoints. That comparison, not a vendor claim, is the measurement that should drive your production choice. For endpoint-specific recommendations, use the RPC Assistant.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started