Skip to content

Resume with cursors

Persist cursors and resume Substreams consumers after disconnects.

Restart a consumer after a disconnect or deploy without losing or duplicating data. Save each block's cursor in the same transaction as its output, then reconnect with the latest committed cursor. The stream continues at the next block.

Rows and cursor commit in one transaction. After a restart, the consumer reconnects with the stored cursor and continues at the next block without skipping any.

Prerequisites

  • A durable database, checkpoint table, or state store.
  • A consumer that receives BlockScopedData messages and their cursors.
  • Idempotent output writes.

Commit, then resume

For each BlockScopedData message:

  1. Write the block's output.
  2. Save the message's cursor in the same transaction.
  3. Commit, then process the next message.

When the consumer restarts, read the latest committed cursor and pass it to the stream. The CLI tab resumes with --cursor. The complete Go, Rust, and TypeScript consumers resume from GRAPHOPS_CURSOR. Set it to your last committed cursor. They print each decoded output and its cursor; connect that handoff to your database transaction before using the consumer in production.

Prerequisites

  1. Install the Substreams CLI.

  2. Set GRAPHOPS_API_KEY to your GraphOps API key.

  3. Replace <LAST_COMMITTED_CURSOR> with the cursor of the last block you committed.

Substreams CLI

# The CLI sends SUBSTREAMS_API_KEY to GraphOps in the x-api-key header.
export SUBSTREAMS_API_KEY="${GRAPHOPS_API_KEY}"

# --cursor resumes right after the block where that cursor was committed.
substreams run \
  -e "https://1.evm.substreams.graphops.xyz" \
  "https://spkg.io/v1/packages/ethereum-common/v0.4.0" \
  "filtered_events" \
  --params "filtered_events=evt_addr:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 && evt_sig:0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" \
  --start-block 18000002 \
  --stop-block 18000008 \
  --cursor "<LAST_COMMITTED_CURSOR>" \
  --max-retries 0 \
  --force-protocol-version 2 \
  -o jsonl

Output

{  "@module": "filtered_events",  "@block": 18000003,Historical block: 18,000,003  "@type": "sf.substreams.ethereum.v1.Events",  "@data": {    "clock": {      "id": "90c06a9fad66de217ce5862a92a2d45a8f42f9abf8ec17d5bfb85ffdc7836ca4",      "number": "18000003",      "timestamp": "2023-08-26T16:22:11Z"    },    "events": [      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",Ethereum mainnet USDC contract; hexadecimal in CLI output, base64 in Protobuf JSON          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",ERC-20 Transfer signature            "0x000000000000000000000000180cd4303582d348a54fcb0436d5665f082a23d1",Sender address, left-padded to 32 bytes            "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43"Recipient address, left-padded to 32 bytes          ],          "data": "0x00000000000000000000000000000000000000000000000000000000aad0a340",Transfer amount: 2,865,800,000 base units, 2,865.8 USDC          "blockIndex": 222,          "ordinal": "4219"        },        "txHash": "05c9c59145387faa91370e56d717d42856bd20f8f19306814d5b7afbe837efe3"Transaction containing this log      },      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x0000000000000000000000001c727a55ea3c11b0ab7d3a361fe0f3c47ce6de5d",            "0x000000000000000000000000c3dcc088aef4373e665b00fc8be45a4b6a4b24a6"          ],          "data": "0x000000000000000000000000000000000000000000000000000000000686bcda",          "blockIndex": 230,          "ordinal": "4543"        },        "txHash": "eb8505dd3dc2964c1b43ed40371fb9c907b751ccb9294b99db032270889edc42"      }    ]  }}{  "@module": "filtered_events",  "@block": 18000004,  "@type": "sf.substreams.ethereum.v1.Events",  "@data": {    "clock": {      "id": "7f7889c3686d7f560dd690aeb2a10b49e3a76844543cf78cc0dfe4687e2985d2",      "number": "18000004",      "timestamp": "2023-08-26T16:22:23Z"    },    "events": [      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x0000000000000000000000000000000000000000000000000000000000000000",            "0x00000000000000000000000043954785980c50768373dc1dafd7ba3a2052036f"          ],          "data": "0x000000000000000000000000000000000000000000000000000000e8d433aa6e",          "index": 1,          "blockIndex": 191,          "ordinal": "3060"        },        "txHash": "b2c1d8c8e7989adab51dafdc9be24fd05243342b8d08828c4325662de629c690"      }    ]  }}{  "@module": "filtered_events",  "@block": 18000005,  "@type": "sf.substreams.ethereum.v1.Events",  "@data": {    "clock": {      "id": "127c6b9794874d7011770b3aa2a0a640e1db201c2d677d01b2b3b19e7ad427c7",      "number": "18000005",      "timestamp": "2023-08-26T16:22:35Z"    },    "events": [      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x000000000000000000000000c880d8be4e94f644f1daa7d3558b50ff5a1c2db7",            "0x000000000000000000000000ae2d4617c862309a3d75a0ffb358c7a5009c673f"          ],          "data": "0x0000000000000000000000000000000000000000000000000000002e90edd000",          "blockIndex": 64,          "ordinal": "1110"        },        "txHash": "1691b8e89e13f483c549d9b61efbc54ecc54015b8186726911c8458f0993e13d"      },      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x000000000000000000000000b86fc5e985facdcae6034beefc6cbdcd8bbd8cb4",            "0x00000000000000000000000027f713adefc708469d826ab8f5b1e4672f1a070e"          ],          "data": "0x0000000000000000000000000000000000000000000000000000000059682f00",          "blockIndex": 203,          "ordinal": "3669"        },        "txHash": "440106b494b7eb657d19e5f82a164ce4cc62e5252058fab5fb4ee6b92600543c"      }    ]  }}{  "@module": "filtered_events",  "@block": 18000006,  "@type": "sf.substreams.ethereum.v1.Events",  "@data": {    "clock": {      "id": "ed05b59286d4f2db801e78a23ce4584d869d857b70b2758822844427caa55a85",      "number": "18000006",      "timestamp": "2023-08-26T16:22:47Z"    }  }}{  "@module": "filtered_events",  "@block": 18000007,  "@type": "sf.substreams.ethereum.v1.Events",  "@data": {    "clock": {      "id": "db63e26b655ad31d60b2173c02127180ed46b28506f785a2682e058ca61afdc1",      "number": "18000007",      "timestamp": "2023-08-26T16:22:59Z"    },    "events": [      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x000000000000000000000000b4eae88d1643e172518dde0ef213c20628798c65",            "0x00000000000000000000000038c5a4f22e2e290ba9bbb9e3d9aaf8582f79f4d1"          ],          "data": "0x0000000000000000000000000000000000000000000000000000000006b78840",          "blockIndex": 94,          "ordinal": "1448"        },        "txHash": "21d8e836f79606db39fcd51f7e563ea9c12cfb7a2bb3e36f23f4eb89b93045ea"      },      {        "log": {          "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",          "topics": [            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",            "0x000000000000000000000000579752cff8fee7af09446b2133ee2f9ff10c4fbf",            "0x0000000000000000000000001715a3e4a142d8b698131108995174f37aeba10d"          ],          "data": "0x000000000000000000000000000000000000000000000000000000001d0221cd",          "blockIndex": 179,          "ordinal": "3398"        },        "txHash": "d78a180e669534a650d842f13d032610e29bf4a7731fb90a6dff73d752ac049c"      }    ]  }}

The CLI recording resumes after block 18000002. The other recordings show the programs’ default range without GRAPHOPS_CURSOR; your own cursor changes their output.

Check the resume

After a restart, the first block the stream sends should be the block after the last one you committed. If it is earlier, the consumer saved the cursor before its output or did not save the latest cursor; if it is later, a cursor was saved for output that never committed.

Make replays safe

  • Save the cursor after the output commits, never before.
  • Make writes idempotent. A retry can replay data from the last committed cursor.
  • Store the endpoint, package reference, and module name with the cursor, so you resume the same stream that produced it.
  • Handle BlockUndoSignal separately from normal output; see Cursors and reorgs.
  • Reconnect with exponential backoff and jitter after retryable disconnects.

Next steps