# Backfill historical events

Source: https://docs.graphops.xyz/rpc/guides/backfill-historical-events

Build a transfer history, audit trail, or analytics table from months of contract events, and
resume it after any failure without starting over. This guide scans USDC `Transfer` logs in
bounded block ranges and stores a checkpoint after each range. Older ranges need archive access;
check [Supported networks](../networks#supported-endpoints) for coverage on your network.

## Prerequisites

* An API key that includes RPC.
* Archive access for the target chain.
* A durable store for logs and range progress.
* An idempotency key such as `(blockHash, transactionHash, logIndex)`.

## How the backfill runs

*Ranges 1 and 2 are stored and the checkpoint records block 18,003,839. Range 3 is in flight; after its logs are written, the checkpoint moves to 18,005,759. A restart resumes from the checkpoint and repeats at most one range.*

- Range 1: blocks 18,000,000 to 18,001,919. Stored.
- Range 2: blocks 18,001,920 to 18,003,839. Stored.
- Range 3: blocks 18,003,840 to 18,005,759. In flight.
- Range 4: blocks 18,005,760 to 18,007,679. Queued.
- Checkpoint: `lastCompletedBlock: 18003839`.
- Next request: `eth_getLogs` with `"fromBlock": "0x112b780"` and `"toBlock": "0x112beff"`.

1. Choose a starting block and a range size. This guide uses 1,920 blocks per range.
2. Request one range with `eth_getLogs`, filtered by contract address and event topic.
3. Write the returned logs idempotently, keyed by `(blockHash, transactionHash, logIndex)`.
4. Store the range's last block as your checkpoint, then request the next range.

Run one live consumer separately for new blocks, such as a
[WebSocket subscription](./websocket-subscriptions), and stop the backfill where the live consumer
starts.

## Request

Each variant requests the first range, blocks 18,000,000 to 18,001,919. Repeat the same request
shape for each following range.

CLI · curl

Prerequisites

1. Install jq.

2. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with jq 1.8.2.

[Example project](/examples/rpc.guides.backfill-historical-events.sh-1.tar.gz)

```sh
# The first 1,920-block range: 0x112a880-0x112afff is blocks 18,000,000-18,001,919.
# The topic is keccak256("Transfer(address,address,uint256)"), the Transfer event signature.
curl --fail-with-body --silent --show-error "https://rpc.graphops.xyz/v1/evm/1" \
  -H "Authorization: Bearer ${GRAPHOPS_API_KEY}" \
  -H "Content-Type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getLogs",
    "params": [
      {
        "fromBlock": "0x112a880",
        "toBlock": "0x112afff",
        "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "topics": [
          "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
        ]
      }
    ]
  }' | jq '{logCount: (.result | length)}'

```

TypeScript · viem

Prerequisites

1. Install Node.js.

2. Install tsx and viem. `npm install tsx viem`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Node.js 24.22.1, tsx 4.23.15, viem 2.57.10.

[Example project](/examples/rpc.guides.backfill-historical-events.ts-2.tar.gz)

```typescript
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http("https://rpc.graphops.xyz/v1/evm/1", {
    fetchOptions: {
      headers: {
        Authorization: `Bearer ${process.env.GRAPHOPS_API_KEY}`,
      },
    },
  }),
});

// The first 1,920-block range. Request each following range the same way.
const logs = await client.getLogs({
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  event: parseAbiItem(
    "event Transfer(address indexed from, address indexed to, uint256 value)",
  ),
  fromBlock: 18_000_000n,
  toBlock: 18_001_919n,
});

console.log(JSON.stringify({ logCount: logs.length }));
// Store logs idempotently, then save toBlock as the checkpoint in your durable store.

```

Go · ethclient

Prerequisites

1. Install Go.

2. Install github.com/ethereum/go-ethereum. `go get github.com/ethereum/go-ethereum`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Go 1.26.0, github.com/ethereum/go-ethereum v1.17.7.

[Example project](/examples/rpc.guides.backfill-historical-events.go-6.tar.gz)

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"math/big"
	"os"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/rpc"
)

func main() {
	ctx := context.Background()
	rpcClient, err := rpc.DialOptions(ctx, "https://rpc.graphops.xyz/v1/evm/1",
		rpc.WithHeader("Authorization", "Bearer "+os.Getenv("GRAPHOPS_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	client := ethclient.NewClient(rpcClient)
	defer client.Close()

	// The first 1,920-block range. Request each following range the same way.
	logs, err := client.FilterLogs(ctx, ethereum.FilterQuery{
		FromBlock: big.NewInt(18_000_000),
		ToBlock:   big.NewInt(18_001_919),
		Addresses: []common.Address{common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")},
		// keccak256("Transfer(address,address,uint256)"), the Transfer event topic.
		Topics: [][]common.Hash{{
			common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
		}},
	})
	if err != nil {
		log.Fatal(err)
	}

	summary, err := json.Marshal(map[string]int{"logCount": len(logs)})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(summary))
	// Store logs idempotently, then save toBlock as the checkpoint in your durable store.
}

```

Rust · alloy

Prerequisites

1. Install Rust.

2. Install alloy, reqwest, tokio, and serde\_json. `cargo add alloy reqwest tokio serde_json -F alloy/provider-ws -F alloy/sol-types -F alloy/contract -F reqwest/json -F tokio/macros -F tokio/rt-multi-thread`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Rust 1.98.1, alloy 2.5.0, reqwest 0.13.5, tokio 1.48.0, serde\_json 1.0.145.

[Example project](/examples/rpc.guides.backfill-historical-events.rust-7.tar.gz)

```rust
use alloy::primitives::address;
use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::client::RpcClient;
use alloy::rpc::types::Filter;
use alloy::transports::http::Http;
use reqwest::header::{HeaderMap, AUTHORIZATION};
use serde_json::json;
use std::{env, error::Error};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut headers = HeaderMap::new();
    headers.insert(
        AUTHORIZATION,
        format!("Bearer {}", env::var("GRAPHOPS_API_KEY")?).parse()?,
    );
    let http = reqwest::Client::builder()
        .default_headers(headers)
        .build()?;
    let provider = ProviderBuilder::new().connect_client(RpcClient::new(
        Http::with_client(http, "https://rpc.graphops.xyz/v1/evm/1".parse()?),
        false,
    ));

    // The first 1,920-block range. Request each following range the same way.
    let filter = Filter::new()
        .address(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"))
        .event("Transfer(address,address,uint256)")
        .from_block(18_000_000)
        .to_block(18_001_919);
    let logs = provider.get_logs(&filter).await?;

    println!("{}", json!({ "logCount": logs.len() }));
    // Store logs idempotently, then save to_block as the checkpoint in your durable store.
    Ok(())
}

```

Python · web3.py

Prerequisites

1. Install Python.

2. Install web3. `python -m pip install web3`

3. Set GRAPHOPS\_API\_KEY to your GraphOps API key.

Tested with Python 3.12.12, web3 8.0.0.

[Example project](/examples/rpc.guides.backfill-historical-events.python-4.tar.gz)

```python
import json
import os
from web3 import Web3

w3 = Web3(
    Web3.HTTPProvider(
        "https://rpc.graphops.xyz/v1/evm/1",
        request_kwargs={
            "headers": {
                "Authorization": f"Bearer {os.environ['GRAPHOPS_API_KEY']}",
            },
        },
    )
)

# The first 1,920-block range. Request each following range the same way.
logs = w3.eth.get_logs(
    {
        "fromBlock": 18_000_000,
        "toBlock": 18_001_919,
        "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        # keccak256("Transfer(address,address,uint256)"), the Transfer event topic.
        "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
        ],
    }
)

print(json.dumps({"logCount": len(logs)}))
# Store logs idempotently, then save toBlock as the checkpoint in your durable store.

```

## Range sizing

Start with small ranges and widen them only after observing response size and latency. Avoid
unbounded `fromBlock` to `latest` scans; a failed broad request repeats all of its work.

| Situation          | Action                                          |
| ------------------ | ----------------------------------------------- |
| Timeout            | Reduce range size.                              |
| Response too large | Reduce range size or add more selective topics. |
| Sparse logs        | Increase range size gradually.                  |
| Rate limit (`429`) | Lower concurrency and add backoff with jitter.  |

## Progress tracking

Store the checkpoint in the same transaction as the range's logs, so a restart never skips a
range. After the first range, the record looks like this:

```json
{
  "network": "ethereum",
  "chainId": 1,
  "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
  "lastCompletedBlock": 18001919
}

```

On restart, read `lastCompletedBlock` and request the next range from the block after it.

## Next steps

* [Archive reads](../concepts/archive-reads): Check which methods and ranges need archive access.
* [Usage and limits](../usage-and-limits): Size worker concurrency within your organization's shared limits.
* [WebSocket subscriptions](./websocket-subscriptions): Keep the history current with live events once the backfill catches up.