WritingSubgraph Performance Best Practices: Faster Indexing and Lower Query Latency — Clixo
5 min readsubgraph, performance, blockchain-indexing, the-graph, best-practices

Subgraph Performance Best Practices: Faster Indexing and Lower Query Latency

Practical subgraph performance best practices — avoiding eth_calls, pruning, immutable entities, and schema design — to reduce sync time and query latency.

Your subgraph deployed fine on testnet. On mainnet, historical sync takes three days, and queries on the live UI are timing out under moderate load. This is not a tooling problem — it is a schema and handler design problem. The Graph indexing node is deterministic and well-engineered; the bottlenecks are almost always in the code and data model you wrote.

Here are the most impactful subgraph performance best practices, ordered by the leverage they provide.

1. Eliminate eth_calls from Event Handlers

This is the single highest-impact optimization. An eth_call inside a handler is a synchronous RPC call to the contract being indexed. During historical sync, which may cover millions of blocks, every eth_call adds latency to every handler invocation.

The common case: fetching token name, symbol, and decimals for each new token in a PairCreated or Transfer handler.

Wrong approach: call ERC20.bind(event.params.token).symbol() inside every handler.

Correct approach: call it once when the token entity is first created, store the result on the entity, and read the stored value in all subsequent handlers. If the token entity does not exist yet when a secondary event arrives, create a placeholder and fill in the metadata from a dedicated initialization handler.

A subgraph with even a handful of eth_calls per handler can index ten times slower than an equivalent subgraph that avoids them entirely.

2. Use Immutable Entities for Append-Only Data

The @entity(immutable: true) directive tells the indexer that an entity will never be updated after creation. This removes the MVCC (multi-version concurrency control) overhead — the node does not need to track historical versions of the entity for time-travel queries.

Raw event records are the clearest candidates:

type SwapEvent @entity(immutable: true) {
  id: Bytes!
  pool: Pool!
  amountIn: BigDecimal!
  amountOut: BigDecimal!
  blockNumber: BigInt!
  timestamp: BigInt!
}

Aggregate entities (pools, users, protocol-level counters) must remain mutable. But splitting your schema into immutable event entities and mutable aggregate entities can meaningfully reduce index overhead.

Subgraph Performance Best Practices for Schema Design

3. Use Bytes IDs Instead of String IDs

Entity IDs of type Bytes! (rather than ID! which maps to String) use a more efficient internal representation. The Graph documentation explicitly recommends Bytes! for IDs derived from transaction hashes and addresses. This is a small change that compounds across millions of entities.

4. Avoid Deep Nesting in GraphQL Queries

Subgraph GraphQL queries that traverse multiple levels of entity relationships (pool → swaps → users → positions) generate multiple database joins per request. Design your schema so that common UI queries can be satisfied with one or two entity levels.

Where you need aggregate data, compute it at write time (in the handler) and store it on the parent entity rather than deriving it at query time.

5. Set startBlock to the Contract Deployment Block

A subgraph that starts from block 0 scans the entire chain history before reaching your contract. On Ethereum mainnet, that is over 20 million blocks. Set startBlock in your manifest to the exact block in which your contract was deployed (or the factory that created it). This alone can cut initial sync time from days to hours for newer contracts.

6. Enable Pruning to Manage Database Size

As a subgraph accumulates history, the database grows. Without pruning, the indexer stores every historical version of every entity, which inflates storage and slows down both indexing and queries over time.

Enable pruning in your subgraph configuration to retain only recent history (e.g., the last 7 days of entity versions). The appropriate retention window depends on your query patterns — if your API only serves current state, a short window is fine. If time-travel queries (entity state at block N) are required, disable pruning or set a longer window.

7. Minimize the Number of Indexed Events

Every event listed in subgraph.yaml under eventHandlers causes the indexer to fetch and process logs for that event signature across the indexed block range. If you have event handlers that do minimal or no work (e.g., a handler that only emits a log for debugging), remove them. Unused event handlers still consume indexing time.

8. Avoid Redundant .load() Calls

Inside a handler, each call to Entity.load(id) is a database read. If you call Pool.load(poolId) three times in the same handler, you are making three reads for the same record. Load once, use the reference.

For handlers that create entities conditionally (create if not exists), the pattern is:

let pool = Pool.load(id);
if (!pool) {
  pool = new Pool(id);
  pool.swapCount = BigInt.fromI32(0);
}
pool.swapCount = pool.swapCount.plus(BigInt.fromI32(1));
pool.save();

This is a single load and a single save.

9. Monitor Handler Execution Time During Development

Before deploying to mainnet sync, run your subgraph locally against a forked or testnet environment and observe handler execution time. Unusually slow handlers are a signal that eth_calls or redundant loads are present. Catch this before committing to a multi-day historical sync on mainnet.

10. Test Reorg Behavior Explicitly

Performance under load also includes correctness under reorgs. A subgraph that does not handle block reorganizations will silently accumulate incorrect state. The Graph node handles basic reorg rollbacks, but complex handler logic that modifies multiple entities must be written so that the rollback leaves the database in a consistent state.


Subgraph performance problems are almost always fixable without switching tooling — they are design problems with known solutions. If you are inheriting a slow subgraph or designing one for a high-throughput protocol, Clixo can audit the data model and handler logic and get sync time under control.