SubscribeCheckpoints - Real-Time Checkpoint Streaming
Stream finalized Sui checkpoints with the gRPC Subscription Service. Learn how field masks, client-side transaction filtering, and gap recovery work.
Stream Finalized Sui Checkpoints
SubscriptionService.SubscribeCheckpoints is a server-streaming RPC that
delivers finalized Sui checkpoints without polling. When a subscription starts,
the server begins with its latest executed checkpoint. While the connection is
active, responses arrive in order and without gaps.
Global stream, not a filtered subscription
SubscribeCheckpoints emits every checkpoint. The standard Sui request does
not support server-side filters for wallets, transactions, packages, modules,
Move functions, objects, or event types. Use a field mask to reduce the fields
returned for each checkpoint, then filter the selected data client-side.
Method Signature
- Service:
sui.rpc.v2.SubscriptionService - Method:
SubscribeCheckpoints - Type: Server-streaming RPC
The generated language examples are abbreviated gap-detection skeletons. They show the request and response shapes, but intentionally omit a production reconnect loop, historical fetch implementation, and bounded backpressure. Follow Recover After a Disconnect before using them in a lossless pipeline. Generate all Sui v2 client stubs with the language-specific setup before running the Python example.
The request has one optional field:
message SubscribeCheckpointsRequest {
optional google.protobuf.FieldMask read_mask = 1;
}Each streamed response wraps the selected checkpoint data with a cursor:
message SubscribeCheckpointsResponse {
optional uint64 cursor = 1;
optional Checkpoint checkpoint = 2;
}The cursor identifies the checkpoint position in the live stream. It is useful for detecting gaps and deciding where a historical backfill must begin, but it is not a resume cursor that can be supplied to a new subscription.
Choose a Field Mask
A field mask controls response shape, not stream selection. Every active subscription still receives one response for every checkpoint.
| Monitoring goal | Suggested checkpoint field paths |
|---|---|
| Checkpoint identity and time | sequence_number, digest, summary.timestamp |
| Transaction confirmation | transactions.digest |
| Transactions sent by a wallet | transactions.digest, transactions.transaction.sender |
| Wallet balance changes | transactions.digest, transactions.balance_changes |
| Move calls | transactions.digest, transactions.transaction.kind |
| Emitted events | transactions.digest, transactions.events.events |
| Changed object owners | transactions.digest, transactions.effects.changed_objects |
Tracking every way a wallet can be affected usually requires more than the transaction sender. Include balance changes and changed-object ownership when you need incoming transfers, object mutations, or other indirect effects.
Subscribe with TypeScript
This example requests checkpoint identity, timestamp, transaction senders, balance changes, Move commands, and events. It serializes processing, preserves the last successfully processed cursor, and stops live processing when it finds a gap. Add bounded storage for the illustrated buffer in production.
const request = {
read_mask: {
paths: [
'sequence_number',
'digest',
'summary.timestamp',
'transactions.digest',
'transactions.transaction.sender',
'transactions.transaction.kind',
'transactions.balance_changes',
'transactions.events.events'
]
}
};
let lastProcessedCursor: bigint | undefined;
let recovering = false;
const bufferedResponses = [];
let processing = Promise.resolve();
const stream = client.SubscribeCheckpoints(request, metadata);
stream.on('data', (response) => {
processing = processing.then(() => handleCheckpoint(response)).catch((error) => {
console.error('Checkpoint processing failed', error);
});
});
async function handleCheckpoint(response) {
if (recovering) {
bufferedResponses.push(response);
return;
}
const checkpoint = response.checkpoint;
if (!checkpoint) return;
const cursor = BigInt(response.cursor);
if (lastProcessedCursor !== undefined && cursor <= lastProcessedCursor) {
console.log('Skipping duplicate or stale checkpoint', response.cursor);
return;
}
if (lastProcessedCursor !== undefined && cursor > lastProcessedCursor + 1n) {
recovering = true;
bufferedResponses.push(response);
console.warn('Checkpoint gap detected', {
firstMissing: (lastProcessedCursor + 1n).toString(),
lastMissing: (cursor - 1n).toString()
});
// Run the recovery sequence below, then drain bufferedResponses in cursor
// order before clearing recovering.
return;
}
console.log('Checkpoint received', {
cursor: response.cursor,
sequenceNumber: checkpoint.sequence_number,
digest: checkpoint.digest,
timestamp: checkpoint.summary?.timestamp,
transactionCount: checkpoint.transactions?.length ?? 0
});
await processMatchingTransactions(checkpoint);
lastProcessedCursor = cursor;
}
stream.on('error', (error) => {
console.error('Checkpoint stream failed', {
code: error.code,
message: error.message,
lastProcessedCursor: lastProcessedCursor?.toString()
});
});Generated clients can expose protobuf field names differently. The examples on
this page use snake_case, matching clients configured with keepCase: true.
Filter Transactions Client-Side
The stream has no predicate in its request. Apply wallet, Move call, and event filters after receiving each checkpoint:
function normalizeSuiId(value: string | undefined): string {
if (!value) return '';
const hex = value.toLowerCase().replace(/^0x/, '');
return `0x${hex.padStart(64, '0')}`;
}
const watchedAddress = normalizeSuiId('0x...');
const watchedPackage = normalizeSuiId('0x2');
const watchedModule = 'coin';
const watchedEventType = `${watchedPackage}::${watchedModule}::WatchedEvent`;
async function processMatchingTransactions(checkpoint: any): Promise<void> {
for (const executed of checkpoint.transactions ?? []) {
const sentByWallet =
normalizeSuiId(executed.transaction?.sender) === watchedAddress;
const changedWalletBalance = (executed.balance_changes ?? []).some(
(change: any) =>
normalizeSuiId(change.address) === watchedAddress
);
const calledWatchedModule = (
executed.transaction?.kind?.programmable_transaction?.commands ?? []
).some((command: any) => {
const moveCall = command.move_call;
return (
normalizeSuiId(moveCall?.package) === watchedPackage &&
moveCall?.module === watchedModule
);
});
const emittedWatchedEvent = (executed.events?.events ?? []).some(
(event: any) => {
const emittedByWatchedModule =
normalizeSuiId(event.package_id) === watchedPackage &&
event.module === watchedModule;
const matchesDeclaredEventType = event.event_type === watchedEventType;
return emittedByWatchedModule || matchesDeclaredEventType;
}
);
if (sentByWallet || changedWalletBalance || calledWatchedModule || emittedWatchedEvent) {
await handleMatchingTransaction(executed);
}
}
}event.package_id and event.module identify the top-level Move call that
triggered emission. event.event_type identifies the declared event type.
Those packages can differ, so retain only the predicate that matches the
monitoring requirement.
Field masks keep this processing efficient by omitting transaction signatures, object contents, effects, and other data that the matcher does not need.
Recover After a Disconnect
A new subscription starts at the latest checkpoint known to the server. It does not replay from the previous connection's cursor automatically.
Use this recovery sequence:
- Persist the last processed cursor or checkpoint sequence number.
- Reconnect with exponential backoff.
- Compare every new cursor with the last processed cursor. Skip a duplicate or
stale response when
cursor <= lastProcessedCursor. - If
cursor > lastProcessedCursor + 1, record the missing range and buffer the gap-detecting response and every later response. Do not process or advance the persisted cursor for any buffered response yet. - Call
LedgerService.GetServiceInfoand inspectlowest_available_checkpoint. If the first missing checkpoint is still retained, fetch each missing checkpoint withLedgerService.GetCheckpoint. - If the gap predates
lowest_available_checkpoint, recover from an archival endpoint or your own durable checkpoint store instead. - Process the backfill in order, then drain buffered live data in cursor order.
Advance
lastProcessedCursoronly after each downstream operation succeeds.
Do not assume a reconnect is lossless. The in-order, gap-free guarantee applies to an active subscription, not across separate connections. A retained full node cannot guarantee lossless recovery after a sufficiently long outage unless an archival endpoint or durable checkpoint store covers the missing range.
Operational Guidance
- Use keepalives appropriate for your client and network path.
- Persist progress only after downstream processing succeeds.
- Apply backpressure instead of dropping checkpoints when processing falls behind.
- Request narrow field masks to reduce serialization work and bandwidth.
- Regenerate clients from the current official Sui protobuf definitions when upgrading API versions.
- Use GraphQL for filtered historical queries over transactions and events, as described in Sui's native gRPC guidance; use gRPC for the live global checkpoint stream.
Related Resources
- GetCheckpoint - Backfill a checkpoint by sequence number
- GetTransaction - Retrieve a transaction by digest
- Sui gRPC documentation - Native Sui concepts and usage
- Sui subscription protobuf - Authoritative request and response schema
Need help with real-time streaming? Contact our support team or review the Sui gRPC overview.
SimulateTransaction
Simulate Sui transactions before execution to preview effects, gas costs, and potential errors via gRPC. Essential for safe transaction building with Dwellir.
ExecuteTransaction
Execute signed transactions on Sui blockchain via gRPC. Learn how to submit transactions, handle signatures, and verify execution with Dwellir's high-performance infrastructure.