Skip to main content

gRPC Migration Cookbook

Each recipe pairs a JSON-RPC call with its gRPC replacement and explains the key differences. For the full method mapping and decision criteria, see the JSON-RPC Migration Guide. For gRPC concepts and setup, see What is gRPC? and Querying Data with gRPC.

info

JSON-RPC is deprecated. Migrate to either gRPC or GraphQL RPC before the week of July 27, 2026, when JSON-RPC is disabled on Sui Foundation mainnet full nodes. Full decommission, including code removal, is planned for mid-October 2026. For a method mapping, decision criteria, and the full timeline, see the JSON-RPC Migration Guide.

Refer to the list of RPC or data providers that have enabled gRPC on their full nodes or offer GraphQL RPC. Contact a provider directly to request access. If your RPC or data provider doesn’t yet support these data access methods, ask them to enable support or contact the Sui Foundation team on Discord, Telegram, or Slack for help.

  • Install the @mysten/sui TypeScript SDK by running npm install @mysten/sui.
  • Obtain access to a gRPC-enabled full node and note its endpoint.

Create a client once and reuse it:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const client = new SuiGrpcClient({
baseUrl: '<FULL_NODE_URL>',
network: 'mainnet',
});

For grpcurl and Buf CLI examples, replace <FULL_NODE_URL> with your provider's gRPC endpoint. See Querying Data with gRPC for setup details and language-specific client instructions for Go and Python.

Get a single object

Replace sui_getObject with LedgerService.GetObject. Use a field mask to request only the fields you need.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getObject",
"params": [
"0xOBJECT_ID",
{ "showContent": true, "showOwner": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

  • JSON-RPC uses the showContent and showOwner booleans. gRPC uses a read_mask with field paths, or the SDK's include parameter.
  • The SDK's getObject returns { object } directly. Use getObjects (plural) for batch lookups, which returns { objects: (Object | Error)[] }.

Batch-fetch objects

Replace sui_multiGetObjects with LedgerService.BatchGetObjects. The top-level read_mask applies to all objects in the batch.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetObjects",
"params": [
["0xOBJECT_A", "0xOBJECT_B", "0xOBJECT_C"],
{ "showContent": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

  • The service respects only the top-level read_mask. The service ignores any mask inside individual sub-requests.
  • The response returns objects in the same order as the request.

Get a transaction

Replace sui_getTransactionBlock with LedgerService.GetTransaction.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getTransactionBlock",
"params": [
"J4NvV5iQZQFm1xKPYv9ffDCCPW6cZ4yFKsCqFUiDX5L4",
{ "showEffects": true, "showEvents": true }
]
}'

The gRPC call differs from the JSON-RPC call in the following ways:

  • JSON-RPC uses the showEffects and showEvents booleans. gRPC uses read_mask paths or the SDK's include parameter.
  • Digests are Base58-encoded strings in both APIs.

Batch-fetch transactions

Replace sui_multiGetTransactionBlocks with LedgerService.BatchGetTransactions.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_multiGetTransactionBlocks",
"params": [
["DIGEST_A", "DIGEST_B"],
{ "showEffects": true, "showEvents": true }
]
}'

The same read_mask rules apply as for batch objects: the service respects only the top-level mask.

List and paginate transactions

Replace suix_queryTransactionBlocks with LedgerService.ListTransactions for filtered, paginated queries over a checkpoint range. Unlike StateService list APIs (which return a single page), ListTransactions returns a server-side stream. Each response frame carries a watermark with a resume cursor, and the final frame includes a QueryEnd with the reason the stream ended.

info

The @mysten/sui TypeScript SDK (v2.23.1+) exposes client.listTransactions() as a high-level wrapper. You can also use the proto client directly through client.ledgerService.listTransactions(). The grpcurl example below shows the raw protobuf JSON request shape.

$ grpcurl -d '{
"start_checkpoint": "1000000",
"filter": {
"terms": [{
"literals": [{
"sender": { "address": "<ADDRESS>" }
}]
}]
},
"options": { "limit": 50 },
"read_mask": { "paths": ["digest", "effects"] }
}' <FULL_NODE_URL> sui.rpc.v2.LedgerService/ListTransactions

The List APIs differ from JSON-RPC pagination in the following ways:

  • ListTransactions is a server-side stream. Each response frame contains a transaction (the matched item), a watermark (progress marker), and optionally a QueryEnd (on the final frame).
  • The watermark.cursor is an opaque binary token. Pass it as options.after to resume ascending pagination, or options.before for descending.
  • The watermark.checkpoint field tells you how much of the checkpoint range the server scanned, which is useful for sparse filters that produce few results across many checkpoints.
  • The QueryEnd.reason tells you why the stream ended:
    • ITEM_LIMIT: returned the requested number of items. Use the cursor to fetch the next page.
    • SCAN_LIMIT: the server's internal scan budget was exhausted before finding enough matches. Resume from the cursor to continue scanning.
    • LEDGER_TIP: reached the current tip of the chain (no end_checkpoint was specified).
    • CHECKPOINT_BOUND: reached the specified end_checkpoint.
    • CURSOR_BOUND: reached a cursor-derived boundary (for example, the options.before cursor used during backfill).

ListCheckpoints and ListEvents follow the same streaming pattern with the same watermark and QueryEnd semantics. ListCheckpoints accepts a TransactionFilter (filtering checkpoints that contain matching transactions), while ListEvents accepts an EventFilter.

Query events

JSON-RPC uses suix_queryEvents with filters such as MoveModule, MoveEventType, Sender, and Transaction. gRPC provides LedgerService.ListEvents for paginated, filtered historical queries and SubscriptionService.SubscribeEvents for live streaming with the same filters. You can also read events from a specific transaction.

Events from a known transaction

If you know the transaction digest, fetch the transaction with events included.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_queryEvents",
"params": [
{ "Transaction": "8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2" },
null, 50, false
]
}'

Live event streaming

Replace sui_subscribeEvent (WebSocket) with SubscriptionService.SubscribeEvents. The JSON-RPC WebSocket subscription APIs have been deprecated since mid-2024, so migrate any remaining WebSocket subscriptions to gRPC streaming. The server filters events before it sends them, so your client receives only matching events.

// Old WebSocket subscription (no longer supported)
const ws = new WebSocket('wss://<FULL_NODE_URL>/websocket');
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sui_subscribeEvent',
params: [{ MoveEventType: '<PACKAGE>::module::MyEvent' }],
}));
ws.onmessage = (msg) => console.log(JSON.parse(msg.data));

The gRPC subscription differs from the JSON-RPC subscription in the following ways:

  • JSON-RPC WebSocket subscriptions filtered events server-side, and have been deprecated since mid-2024. gRPC SubscribeEvents also filters server-side using an EventFilter, so your client receives only matching events.
  • Subscriptions begin at the current tip and do not accept a resume point. Filtered subscriptions start with a progress-only frame whose cursor marks the point immediately before live delivery; an unfiltered subscription can start with a payload, so never discard the first frame. Immediately consume live frames into durable, capacity-managed storage while LedgerService.ListEvents backfills to that boundary. If the spool fills or the stream drops, restart from the last durably processed cursor and backfill the gap. See Subscriptions for streaming data.
  • For filtered historical event queries over a range, use LedgerService.ListEvents or GraphQL Query.events.

Query events over a checkpoint range

Use LedgerService.ListEvents to retrieve events matching an EventFilter across a range of checkpoints. This method replaces the paginated suix_queryEvents pattern.

info

The @mysten/sui TypeScript SDK (v2.23.1+) exposes client.listEvents() as a high-level wrapper. You can also use the proto client directly through client.ledgerService.listEvents(). The grpcurl example in the next tab shows the raw protobuf JSON request shape.

// Use the generated proto client directly
const stream = client.ledgerService.listEvents({
startCheckpoint: BigInt(1000000),
endCheckpoint: BigInt(1000100),
filter: {
terms: [{
literals: [{
negated: false,
predicate: {
oneofKind: 'eventType',
eventType: { eventType: '<PACKAGE>::module::MyEvent' },
},
}],
}],
},
options: { limit: 50 },
});

for await (const response of stream.responses) {
console.log('Event:', response.event);
}

Stream checkpoints

Replace sui_getCheckpoints polling with SubscriptionService.SubscribeCheckpoints for a real-time, ordered stream, or LedgerService.ListCheckpoints for paginated queries over a checkpoint range.

// Old polling pattern (inefficient and deprecated)
let cursor = null;
while (true) {
const res = await fetch('<FULL_NODE_URL>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0', id: 1,
method: 'sui_getCheckpoints',
params: [cursor, 10, false],
}),
});
const { result } = await res.json();
for (const cp of result.data) {
console.log('Checkpoint:', cp.sequenceNumber);
cursor = cp.sequenceNumber;
}
if (!result.hasNextPage) await new Promise((r) => setTimeout(r, 1000));
}

The gRPC stream differs from the JSON-RPC polling pattern in the following ways:

  • You do not need a polling loop. The gRPC stream pushes checkpoints as they finalize.
  • Use LedgerService.ListCheckpoints to query a historical range of checkpoints, optionally filtered by a TransactionFilter. Use SubscriptionService.SubscribeCheckpoints for live streaming from the current tip.
  • Subscriptions do not support resumption. Filtered subscriptions start with a progress-only frame whose cursor marks the point immediately before live delivery; an unfiltered subscription can start with a payload, so never discard the first frame. Open the subscription first and immediately drain live frames into durable, bounded storage while ListCheckpoints backfills to the starting boundary. If the spool fills or the stream drops, restart from the last durably processed cursor and backfill the gap.
  • grpcurl supports server-side streaming for the LedgerService.List* calls on this page. For SubscriptionService subscriptions, which are indefinite streams, the Buf CLI or an SDK client provides better control over timeouts and reconnection.

Stream transactions

Use SubscriptionService.SubscribeTransactions for a real-time, filtered stream of transactions. This replaces both JSON-RPC WebSocket subscriptions and polling patterns.

info

The @mysten/sui TypeScript SDK (v2.23.1+) exposes SubscribeTransactions through the proto client at client.subscriptionService.subscribeTransactions(). The Buf CLI example below shows the raw protobuf JSON request shape.

$ buf curl --protocol grpc \
https://<FULL_NODE_URL>/sui.rpc.v2.SubscriptionService/SubscribeTransactions \
-d '{
"filter": {
"terms": [{
"literals": [{
"sender": { "address": "<ADDRESS>" }
}]
}]
},
"read_mask": { "paths": ["digest", "effects"] }
}' \
--timeout 5m

Subscriptions begin at the current tip of the chain. A filtered subscription starts with a progress-only frame (no transaction payload) that establishes the stream's start position. An unfiltered subscription can start with a transaction payload, so do not discard its first frame. To avoid gaps between historical data and the live stream, see Backfill and subscribe without gaps.

Look up a single checkpoint

Replace sui_getCheckpoint with LedgerService.GetCheckpoint.

info

A full node serves only checkpoints within its retention window. If you query an old sequence number and receive NOT_FOUND, either use a more recent checkpoint or query the Archival Service instead.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_getCheckpoint",
"params": ["CHECKPOINT_SEQUENCE_NUMBER"]
}'

Get balances

Replace suix_getBalance and suix_getAllBalances with StateService.GetBalance and StateService.ListBalances.

To query a single coin type, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getBalance",
"params": ["<ADDRESS>", "0x2::sui::SUI"]
}'

To query all coin types, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getAllBalances",
"params": ["<ADDRESS>"]
}'

gRPC separates coinBalance (coin objects) and addressBalance (accumulator). The balance field is the combined total. See Using Address Balances for reconciliation details.

List owned coins

Replace suix_getCoins and suix_getAllCoins with StateService.ListOwnedObjects filtered by coin type.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getCoins",
"params": ["<ADDRESS>", "0x2::sui::SUI", null, 50]
}'

The gRPC approach differs from the JSON-RPC approach in the following ways:

  • There is no dedicated coin-listing RPC. Use ListOwnedObjects with a Coin<T> type filter.
  • To list coins of every type, filter by 0x2::coin::Coin without a type parameter.
  • For transaction building, prefer gas smashing or address-balance gas payments over manual coin selection.

List dynamic fields

Replace suix_getDynamicFields with StateService.ListDynamicFields.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getDynamicFields",
"params": ["0xPARENT_OBJECT_ID", null, 50]
}'

For suix_getDynamicFieldObject, derive the dynamic field's object ID locally from the parent ID and field name, then call LedgerService.GetObject. You do not need ListDynamicFields when you already know the field name.

Execute a transaction

Replace sui_executeTransactionBlock with TransactionExecutionService.ExecuteTransaction.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_executeTransactionBlock",
"params": [
"BASE64_TX_BYTES",
["BASE64_SIGNATURE"],
{ "showEffects": true }
]
}'

The JSON-RPC unsafe_* builder methods, such as unsafe_moveCall and unsafe_transferObject, have no gRPC equivalent. Build transactions with programmable transaction blocks (PTBs) using the SDK, then submit the bytes. See Building PTBs and Signing and Sending Transactions.

Sign and execute a transaction (end-to-end)

Replace the JSON-RPC signAndExecuteTransactionBlock pattern with signAndExecuteTransaction on the gRPC client. This method builds the transaction (including gas resolution), signs it, and executes it in a single call.

// Old JSON-RPC pattern
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { Transaction } from '@mysten/sui/transactions';

const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
const keypair = new Ed25519Keypair();

const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [1_000_000]);
tx.transferObjects([coin], '<RECIPIENT_ADDRESS>');

const result = await client.signAndExecuteTransaction({
signer: keypair,
transaction: tx,
});
console.log('Digest:', result.digest);

The gRPC call differs from the JSON-RPC call in the following ways:

  • Both use signAndExecuteTransaction, but the gRPC version accepts signer (a keypair) and transaction (a Transaction object or raw bytes).
  • The gRPC version uses include to select which fields the response contains, instead of request options.
  • The response is a discriminated union: check result.$kind for 'Transaction' (success) or 'FailedTransaction' (failure).
  • Gas resolution happens automatically during build. The SDK calls SimulateTransaction with doGasSelection: true to select gas coins and set the gas budget.

Manual gas resolution

For sponsored transactions or when you need to select specific gas coins, set gas parameters before building:

// Get the current reference gas price
const { referenceGasPrice } = await client.core.getReferenceGasPrice();

const tx = new Transaction();
tx.setSender(keypair.toSuiAddress());
tx.setGasPrice(BigInt(referenceGasPrice));
tx.setGasBudget(10_000_000);

// Select specific gas coins
tx.setGasPayment([{
objectId: '0xGAS_COIN_ID',
version: '1',
digest: 'COIN_DIGEST',
}]);

// Add your commands
const [coin] = tx.splitCoins(tx.gas, [1_000_000]);
tx.transferObjects([coin], '<RECIPIENT_ADDRESS>');

// Build and sign manually
const bytes = await tx.build({ client });
const { signature } = await keypair.signTransaction(bytes);

const result = await client.core.executeTransaction({
transaction: bytes,
signatures: [signature],
include: { effects: true },
});

For sponsored transactions, both the sender and sponsor must sign the same full transaction bytes (including gas fields). The sponsor sets setGasPayment and setGasOwner, then both parties sign the complete built transaction. See Sponsored Transactions for the full pattern.

Simulate a transaction (dry run)

Replace sui_dryRunTransactionBlock and sui_devInspectTransactionBlock with TransactionExecutionService.SimulateTransaction.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "sui_dryRunTransactionBlock",
"params": ["BASE64_TX_BYTES"]
}'

Get reference gas price

Replace suix_getReferenceGasPrice with getReferenceGasPrice on the SDK, or call LedgerService.GetEpoch through grpcurl and read the reference_gas_price field.

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getReferenceGasPrice",
"params": []
}'

Resolve a SuiNS name

Replace JSON-RPC name resolution with NameService.LookupName and NameService.ReverseLookupName.

To resolve a name to an address, run the following command:

$ curl -X POST <FULL_NODE_URL> \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_resolveNameServiceAddress",
"params": ["example.sui"]
}'

Get validator APY

JSON-RPC provided suix_getValidatorsApy as a convenience method. Neither gRPC nor GraphQL has a direct APY field or method. Validator APY is derived from staking pool exchange rate history stored in dynamic fields on each validator's system-state object. The legacy suix_getValidatorsApy used a compounding calculation across up to 30 epoch samples with outlier filtering, so a simple two-epoch ratio does not reproduce the same values.

To compute validator APY after migration:

  1. Query the validator set through Epoch.validatorSet in GraphQL or LedgerService.GetEpoch in gRPC.
  2. Read each validator's staking pool exchange rate history from the dynamic fields on the pool's exchange_rates table.
  3. Apply a compounding formula across multiple epoch samples.

See issue #23832 for the recommended computation and the legacy implementation as a reference.

Common patterns

Backfill and subscribe without gaps

To combine historical backfill with a live subscription without missing data:

  1. Open the subscription and immediately consume frames into durable, capacity-managed storage. Do not leave the stream unread during backfill.
  2. Use the first frame's cursor as the live boundary. Filtered subscriptions begin with a progress-only frame whose cursor is the checkpoint immediately before live delivery; unfiltered subscriptions can begin with a payload, which you must also spool.
  3. Backfill from the last durably processed cursor with the matching List* API. Keep the initial before cursor fixed while advancing after from each response watermark until the server returns CURSOR_BOUND. Checkpoint bounds are exclusive at the end, so for a filtered progress cursor at checkpoint C - 1, use endCheckpoint: C.
  4. Process the durable live spool after backfill catches up, deduplicating by cursor. Atomically persist each cursor with the corresponding application updates.
  5. If the spool reaches its capacity or the subscription ends, restart from the last durable cursor and backfill before resuming live processing.

This pattern works with all three subscription and list API pairs: SubscribeCheckpoints with ListCheckpoints, SubscribeTransactions with ListTransactions, and SubscribeEvents with ListEvents. Reuse the same filter for the subscription and historical query.

caution

The server's per-subscriber buffer holds 256 frames and evicts a slow consumer when it fills. A client-side in-memory queue only moves the limit; use a durable bounded spool with explicit overflow recovery for long backfills.

Reconnect a checkpoint stream

If the gRPC stream disconnects, reconnect and drain the new stream into durable storage while a separate worker backfills the gap. Do not await backfill in the subscription loop, because doing so stops the client from reading live frames.

let lastProcessed = await loadLastProcessedCheckpoint();

async function consumeSubscription() {
let attempt = 0;

for (;;) {
try {
const { responses } = client.subscriptionService.subscribeCheckpoints({
readMask: { paths: ['sequenceNumber', 'transactions'] },
});

for await (const response of responses) {
if (response.checkpoint) {
// This durable, bounded spool applies backpressure before its limit
// and signals the backfill worker when the first live frame arrives.
await liveSpool.append(response.checkpoint);
}
}
throw new Error('checkpoint subscription ended');
} catch (err) {
const delay = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * delay * 0.4),
);
attempt++;
}
}
}

async function processWithBackfill() {
for (;;) {
const firstLive = await liveSpool.peek();
await backfillCheckpoints(lastProcessed + 1n, firstLive.sequenceNumber);

for await (const checkpoint of liveSpool.drain()) {
if (checkpoint.sequenceNumber > lastProcessed + 1n) {
// A reconnect can move the live stream past checkpoints that were not
// spooled. Backfill this gap before committing the newer checkpoint.
await backfillCheckpoints(lastProcessed + 1n, checkpoint.sequenceNumber);
}
if (checkpoint.sequenceNumber > lastProcessed) {
// Process idempotently and atomically persist this cursor.
await processAndPersistCheckpoint(checkpoint);
lastProcessed = checkpoint.sequenceNumber;
}
}
}
}

await Promise.all([consumeSubscription(), processWithBackfill()]);

Implement backfillCheckpoints with ListCheckpoints, treating its end checkpoint as exclusive and resuming partial responses from the latest watermark cursor. It must process and durably persist every returned checkpoint before resolving. If the durable spool reaches its configured capacity, stop both workers and restart from lastProcessed rather than dropping live frames. Reset the reconnect attempt only after the stream remains healthy for an application-defined interval; a single frame is not sufficient evidence of recovery.

Paginate through all owned objects

Use the cursor from each response to request the next page:

import type { SuiClientTypes } from '@mysten/sui';

let cursor: string | null = null;

do {
const page: SuiClientTypes.ListOwnedObjectsResponse = await client.core.listOwnedObjects({
owner: '<ADDRESS>',
limit: 50,
cursor,
});

for (const obj of page.objects) {
console.log(obj.objectId);
}

cursor = page.hasNextPage ? page.cursor : null;
} while (cursor);

Fall back to the Archival Store for pruned data

A full node returns NOT_FOUND for data outside its retention window. Fall back to the Archival Store for historical lookups:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const fullNode = new SuiGrpcClient({
baseUrl: '<FULL_NODE_URL>',
network: 'mainnet',
});

const archive = new SuiGrpcClient({
baseUrl: 'https://archive.mainnet.sui.io:443',
network: 'mainnet',
});

async function getTransaction(digest: string) {
const result = await fullNode.core.getTransaction({
digest,
include: { effects: true },
});

if (result.$kind === 'Transaction') {
return result.Transaction;
}

// Data was pruned, so fall back to the Archival Store.
const archiveResult = await archive.core.getTransaction({
digest,
include: { effects: true },
});

if (archiveResult.$kind === 'Transaction') {
return archiveResult.Transaction;
}

throw new Error(`Transaction ${digest} not found in full node or archive`);
}

Troubleshoot streaming connections

gRPC streams can terminate for several reasons. Understanding the server's connection lifecycle helps you build reliable streaming clients.

Connection age limits (GOAWAY): The Sui full node closes connections after a configurable maximum age (default: 4 hours). When a connection reaches this limit, the server sends a GOAWAY frame, stops accepting new streams, and gives in-flight requests a grace period (default: 10 minutes) to complete. Clients observe this as a stream termination. Implement automatic reconnection with backfill from your last processed checkpoint, as shown in Reconnect a checkpoint stream.

Slow consumer eviction: The server buffers up to 256 items per subscriber. If your client cannot keep up and the buffer fills, the server drops the subscription immediately without a graceful close. To avoid this:

  • Process items asynchronously. Write to a queue and process from the queue in a separate worker.
  • Use a read_mask to request only the fields you need, reducing frame size.
  • Use server-side filters to reduce the volume of matching items.

Source lag: If the subscription service falls behind the checkpoint broadcast (for example, due to high server load), the server drops all subscribers. Reconnect and backfill from your last processed checkpoint.

Server-side timeout: Unary requests use the general gRPC timeout, which defaults to 60 seconds. Each streaming List* API has its own configurable timeout-ms; the full node configuration examples use 5 seconds. This deadline covers the entire list stream, so a long backfill can end even after returning frames. Resume from the latest watermark.cursor while preserving the original upper bound. Network, proxy, and GOAWAY interruptions require the same cursor-based recovery.

Public endpoint limitations: The public URLs at https://fullnode.<network>.sui.io are behind load balancers that impose their own connection timeouts and rate limits beyond what the Sui node configures. Long-running streams are more likely to be interrupted on public endpoints. For production streaming workloads, run your own Sui full node or use a provider with a dedicated gRPC endpoint.