Sui JSON-RPC is deprecated. The Sui Foundation switched it off on its Mainnet full nodes in the week of 27 July 2026 and removes the code in mid-October 2026, so every sui_* and suix_* call in your codebase has to move this month. The replacement is gRPC (sui.rpc.v2), which the BoltRPC Sui endpoint already serves. This guide maps every common JSON-RPC method to its gRPC call, swaps the TypeScript client, replaces polling with streams and puts a measured number on what each of those calls costs.
If you are setting Sui up for the first time, start with the Sui RPC endpoint page and come back here when something needs migrating. The rest of this page assumes a key and a JSON-RPC integration that has to move.
What changed: the Sui JSON-RPC shutdown timeline
The deprecation is not a provider decision, it is a protocol one. According to the Sui API reference, the Foundation disabled JSON-RPC on its Mainnet full nodes the week of 27 July 2026 and plans full decommission, including removal of the code from the node software, for mid-October 2026. Once that release ships, a full node no longer knows how to answer sui_getObject, whichever company runs it.
Mysten’s TypeScript SDK followed: SuiJsonRpcClient is marked deprecated and the migration notes point application code at SuiGrpcClient. Two replacements exist:
- gRPC (
sui.rpc.v2): typed requests over HTTP/2, server streams for checkpoints, transactions and events, batch reads. This is the path for apps, wallets, bots and indexers. - GraphQL: flexible filtered queries, no streaming. Mysten recommends it for filtered event queries that do not need a live feed.
This guide covers gRPC. If your only Sui traffic is an occasional filtered event query, read Mysten’s JSON-RPC migration guide for the GraphQL side and skip to the gotchas section here.
On BoltRPC the gRPC door (Beta) has been open since September 2026, on the same key as everything else: native gRPC over HTTP/2 on endpoints.boltrpc.io:443 for the generated Rust, Go and Python clients, plus gRPC-Web at https://endpoints.boltrpc.io/grpc/sui for browsers and for the TypeScript SDK. Both take the API key as x-api-key metadata. Migrate now, on the endpoint you already use.
Measured on BoltRPC: Sui over gRPC, 30 September 2026
Every example in this guide ran against the BoltRPC Sui endpoint on 30 September 2026 through the official @mysten/sui gRPC client (version 2.33). The two lightest checkpoint streams ran for 10 minutes, the heavier read masks and the transaction and event streams are 6 to 15 second samples; all are projected to a day.
| Measurement (30 September 2026) | Result |
|---|---|
| Checkpoint cadence, 10 minute stream | 2,744 checkpoints in 600 s: 4.6 per second, gap between checkpoints p50 109 ms, p90 497 ms, max 1.3 s |
| Stream integrity, 10 minutes, two streams | 0 skipped sequence numbers, 0 reconnects |
| Checkpoints kept on the full node | lowest available checkpoint 4.0 million behind the tip: about ten days of history, the window slid forward by 2,640 checkpoints during the 10 minute run, exactly the cadence |
| Checkpoint stream, minimal read mask (sequence, digest, timestamp) | 76 bytes per checkpoint (10 minute run), about 30 MB a day |
| Checkpoint stream, no read mask | 60 bytes per checkpoint (the server sends a compact default) |
Checkpoint stream, summary | 518 bytes per checkpoint, about 194 MB a day |
| Checkpoint stream, summary plus transaction digests | 1,072 bytes per checkpoint (about 22 transactions each), about 413 MB a day |
Checkpoint stream, full transactions (effects and events) | 177 KB per checkpoint, about 66 GB a day |
| Transaction stream, digest only | about 80 transactions per second at 76 bytes each |
| Event stream, no mask, no filter | about 256 events per second at 147 bytes each |
BatchGetObjects, 45 objects | one call, 15.4 KB (45 single GetObject calls: 15.2 KB) |
BatchGetTransactions, all 26 transactions of one checkpoint | one call, 8.3 KB |
| Deleted object in a batch | reported inline as a per-item result, the call still succeeds |
Read the table bottom up. The read mask is the price lever: the same subscription costs 76 bytes or 177 KB per checkpoint depending on what you ask for, a factor of more than two thousand. Batch reads collapse 45 round trips into one without inflating the payload. And the full node reports exactly how far back its checkpoints go, roughly ten days on the day we measured, so a backfill starts from a known floor instead of a guess.
Method mapping: the calls your app actually makes
Mysten publishes the full mapping. The rows below are the ones that show up in real codebases. On BoltRPC a unary gRPC call is charged like the matching JSON-RPC method, so a migrated read costs what it cost before; the pricing page has the exact rates per call.
| JSON-RPC method | gRPC service and operation |
|---|---|
sui_getLatestCheckpointSequenceNumber | LedgerService.GetCheckpoint (Mysten’s mapping), or GetServiceInfo, which returns checkpoint_height |
sui_getCheckpoint | LedgerService.GetCheckpoint |
sui_getCheckpoints | LedgerService.ListCheckpoints |
sui_getObject, sui_tryGetPastObject | LedgerService.GetObject |
sui_multiGetObjects | LedgerService.BatchGetObjects (bills once, whatever the item count) |
sui_getTransactionBlock | LedgerService.GetTransaction |
sui_multiGetTransactionBlocks | LedgerService.BatchGetTransactions (bills once) |
suix_getBalance, suix_getAllBalances | StateService.GetBalance, StateService.ListBalances |
suix_getCoins, suix_getAllCoins | StateService.ListOwnedObjects with a coin type filter |
suix_getCoinMetadata | StateService.GetCoinInfo |
suix_getOwnedObjects | StateService.ListOwnedObjects |
suix_getDynamicFields | StateService.ListDynamicFields |
suix_queryEvents | LedgerService.ListEvents with an EventFilter |
sui_subscribeEvent, sui_subscribeTransaction | SubscriptionService.SubscribeEvents, SubscribeTransactions |
sui_executeTransactionBlock | TransactionExecutionService.ExecuteTransaction |
sui_dryRunTransactionBlock | TransactionExecutionService.SimulateTransaction |
suix_getReferenceGasPrice | LedgerService.GetEpoch (reference_gas_price) |
suix_getStakes | no direct method: ListOwnedObjects filtered on StakedSui, then GetObject |
List* and Subscribe* calls are charged on the data they deliver rather than per request, at the rate on the pricing page, with opening a stream free. That is why the read mask matters so much for them: it decides how many bytes each message carries.
Two mappings deserve a second look. sui_getLatestCheckpointSequenceNumber had no argument and returned one number; GetServiceInfo also returns the chain identifier, the epoch and the lowest checkpoint the node still holds, so one call at startup replaces three. And suix_queryEvents was a cursor walk; ListEvents takes a typed filter and a page token, which changes how an indexer is structured (see the streams section).
Swap the client: SuiJsonRpcClient to SuiGrpcClient
The TypeScript SDK talks gRPC-Web, so the client points at the gRPC-Web door and passes the key as metadata. This is the whole change for the connection layer:
// Before: deprecated in @mysten/sui
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
const legacy = new SuiJsonRpcClient({
url: 'https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/sui',
network: 'mainnet',
});
// After: gRPC over the gRPC-Web door
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
const baseUrl = 'https://endpoints.boltrpc.io/grpc/sui';
const client = new SuiGrpcClient({
network: 'mainnet',
baseUrl,
transport: new GrpcWebFetchTransport({
baseUrl,
meta: { 'x-api-key': 'YOUR_API_KEY' },
format: 'binary',
}),
});
const { response } = await client.ledgerService.getServiceInfo({});
console.log('tip', String(response.checkpointHeight));
console.log('oldest checkpoint on this node', String(response.lowestAvailableCheckpoint));
console.log('served by', response.server);
Run on 30 September this printed a tip of 328,640,053, an oldest checkpoint of 324,626,935 and boltrpc as the server. The String() calls are not decoration: 64-bit integers arrive as bigint in the SDK. JSON.stringify throws on those. Convert at the edge of your code, once.
The gateway speaks gRPC end to end, errors included: a request without a key gets a proper gRPC status with the message missing API key (x-api-key metadata), so your gRPC error handling sees it instead of an HTTP page. Every error reaches you the same way from here on: a status code plus a message, never a JSON body with an error field.
The generated clients for Rust, Go and Python speak native gRPC over HTTP/2 instead of gRPC-Web. Point them at endpoints.boltrpc.io:443, add x-api-key to the call metadata, keep everything else from Mysten’s examples. Method paths are the standard ones, /sui.rpc.v2.LedgerService/GetCheckpoint and so on.
Reading objects: read masks and batch reads
Every gRPC read takes a read_mask, a list of field paths you want back. Leave it out and the server picks a compact default. Ask for everything and you pay for everything. The object read most apps do on every request looks like this:
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
const baseUrl = 'https://endpoints.boltrpc.io/grpc/sui';
const client = new SuiGrpcClient({
network: 'mainnet',
baseUrl,
transport: new GrpcWebFetchTransport({ baseUrl, meta: { 'x-api-key': 'YOUR_API_KEY' }, format: 'binary' }),
});
// One object, three fields
const { response: single } = await client.ledgerService.getObject({
objectId: '0x5',
readMask: { paths: ['object_id', 'version', 'object_type'] },
});
console.log(single.object?.objectType);
// 0x0000000000000000000000000000000000000000000000000000000000000003::sui_system::SuiSystemState
// Many objects, one call, one bill
const ids = ['0x5', '0x6', '0x403'];
const { response: batch } = await client.ledgerService.batchGetObjects({
requests: ids.map((objectId) => ({ objectId })),
readMask: { paths: ['object_id', 'version', 'object_type', 'owner'] },
});
for (const item of batch.objects) {
if (item.result.oneofKind === 'object') {
console.log(item.result.object.objectId, String(item.result.object.version));
} else {
console.log('not found or deleted:', item.result);
}
}
BatchGetObjects replaces sui_multiGetObjects and bills once whatever the item count. In our run, 45 objects came back in one call of 15.4 KB, where 45 single calls moved 15.2 KB and cost 45 times the weight. One of the 45 objects had been deleted between us collecting the id and asking for it; the batch reported that inline as a per-item result while the other 44 came back normally. The single-call path answers not found for that id, which is the behaviour to design around: batch results are a list of outcomes, not a list of objects.
Version numbers, sequence numbers and balances are all bigint here. Store them as strings or as bigint, never as Number, or a Sui object version above 2^53 will silently round.
Events and transactions: filters instead of cursors
suix_queryEvents was a cursor walk over all events, filtered client side by whatever you passed as the first argument. In gRPC there are two shapes. For history, LedgerService.ListEvents takes an EventFilter and a page token and returns pages at the stream rate. For live data, SubscriptionService.SubscribeEvents takes the same filter and pushes matches as they finalize.
For “the events of a transaction I already know”, read the transaction itself. This is the pattern Mysten recommends and it is one unary call:
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
const baseUrl = 'https://endpoints.boltrpc.io/grpc/sui';
const client = new SuiGrpcClient({
network: 'mainnet',
baseUrl,
transport: new GrpcWebFetchTransport({ baseUrl, meta: { 'x-api-key': 'YOUR_API_KEY' }, format: 'binary' }),
});
const { response } = await client.ledgerService.getTransaction({
digest: 'HwVRMabvfHoN9iUpsgCu3FggvuUDAYPUWC7j3CXkSmkZ',
readMask: {
paths: ['digest', 'effects.status', 'events.events.event_type', 'events.events.package_id'],
},
});
console.log('succeeded:', response.transaction?.effects?.status?.success);
for (const ev of response.transaction?.events?.events ?? []) {
console.log(ev.eventType, ev.packageId);
}
For “all transactions in checkpoint N”, ask the checkpoint for its digests and batch the rest:
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
const baseUrl = 'https://endpoints.boltrpc.io/grpc/sui';
const client = new SuiGrpcClient({
network: 'mainnet',
baseUrl,
transport: new GrpcWebFetchTransport({ baseUrl, meta: { 'x-api-key': 'YOUR_API_KEY' }, format: 'binary' }),
});
const seq = 328640682n;
const { response: cp } = await client.ledgerService.getCheckpoint({
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: seq },
readMask: { paths: ['sequence_number', 'transactions.digest'] },
});
const digests = (cp.checkpoint?.transactions ?? []).map((tx) => tx.digest);
const { response: txs } = await client.ledgerService.batchGetTransactions({
digests,
readMask: { paths: ['digest', 'effects.status', 'events.events.event_type'] },
});
console.log(digests.length, 'transactions,', txs.transactions.length, 'returned');
That checkpoint held 26 transactions; the batch returned all 26 in one 8.3 KB response. Note the checkpointId shape: it is a protobuf oneof, so you name which variant you are sending (sequenceNumber or digest). Positional parameters are gone across the whole API, which is the single most common compile error in a migration and the easiest one to fix.
A live transaction feed with a narrow mask is cheap. In a 6 second sample, SubscribeTransactions with transaction.digest delivered about 80 transactions a second at 76 bytes each: roughly 525 MB a day if you keep it open around the clock. The unfiltered, unmasked SubscribeEvents feed delivered about 256 events a second at 147 bytes each. Put a filter on it, or a mask, or both, before you leave it running.
Streams replace polling and WebSocket subscriptions
Under JSON-RPC, “new checkpoint” meant polling sui_getLatestCheckpointSequenceNumber every second, or on BoltRPC subscribing to the synthesized WebSocket newHeads stream. Our September measurement put those two at roughly 255,000 and 318,000 request units per day on Sui: polling pays for every empty second, the WebSocket stream pays per tick at Sui’s cadence.
SubscribeCheckpoints changes the arithmetic. It is billed by bytes. A checkpoint with a minimal mask is 76 bytes. At 4.6 checkpoints a second that is about 30 MB a day, a heads feed for a full day for a small fraction of what polling once a second costs, with every checkpoint delivered instead of one sample a second. The pricing page converts delivered data into your bill.
The same stream with the full transactions mask is a different product: 177 KB per checkpoint, about 66 GB a day. Priced on delivered data, that lands in the same range as fetching every checkpoint and every transaction by hand, minus all the polling code. It is what a full indexer needs. The point is that you choose, per stream, with the mask.
Subscriptions begin at the tip and do not replay. A production consumer therefore needs three parts: a subscription, a durable “last seen” sequence number and a backfill that closes the gap after a reconnect. GetCheckpoint by sequence number closes gaps one checkpoint at a time; ListCheckpoints does it in pages once you have a range.
import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
const baseUrl = 'https://endpoints.boltrpc.io/grpc/sui';
const mask = { paths: ['sequence_number', 'digest', 'summary.timestamp'] };
const mkClient = () =>
new SuiGrpcClient({
network: 'mainnet',
baseUrl,
transport: new GrpcWebFetchTransport({ baseUrl, meta: { 'x-api-key': 'YOUR_API_KEY' }, format: 'binary' }),
});
let lastSeen: bigint | null = null; // persist this in your own store
async function handle(seq: bigint, digest: string) {
// your indexing logic
lastSeen = seq;
}
async function backfill(client: SuiGrpcClient, from: bigint, to: bigint) {
for (let seq = from; seq < to; seq++) {
const { response } = await client.ledgerService.getCheckpoint({
checkpointId: { oneofKind: 'sequenceNumber', sequenceNumber: seq },
readMask: mask,
});
await handle(seq, response.checkpoint!.digest);
}
}
async function follow() {
for (;;) {
const client = mkClient();
try {
const call = client.subscriptionService.subscribeCheckpoints({ readMask: mask });
for await (const msg of call.responses) {
const cp = msg.checkpoint!;
const seq = cp.sequenceNumber;
if (lastSeen !== null && seq > lastSeen + 1n) {
await backfill(client, lastSeen + 1n, seq); // closes the gap left by a reconnect
}
await handle(seq, cp.digest);
}
} catch (err) {
console.warn('stream closed, resubscribing', String(err));
await new Promise((r) => setTimeout(r, 1000));
}
}
}
follow();
Three details from running this. Checkpoint sequence numbers arrived contiguous within a connection (2,744 in a row over 10 minutes, none skipped), so a gap means a reconnect, not a lost message. Stream slots per key are enforced with a clear gRPC message, concurrent stream limit reached, rather than a silent stall, so a consumer that opens one too many backs off immediately; the allowance per key is on the pricing page. And the gap is bounded by how long you were away, which at 4.6 checkpoints a second is a few hundred GetCheckpoint calls per minute of downtime. Use BatchGetTransactions inside handle if you need transaction bodies, one call per checkpoint.
The synthesized WebSocket feed on Sui keeps working for code that already uses it. eth_subscribe stays native on the EVM chains behind the same key. For new Sui work, the gRPC stream adds full checkpoints priced on bytes.
Where migrations break: six things to fix first
- Typed requests, no positional parameters. Every JSON-RPC call took an array; every gRPC call takes a message with named fields and, for identifiers, a
oneofyou must name (checkpointId: { oneofKind: 'sequenceNumber', ... }). The compiler catches most of this, the rest shows up asinvalid argumenterrors at runtime. - Numbers are
bigint. Sequence numbers, versions, balances and timestamps are 64-bit. The SDK hands youbigint;JSON.stringifyrefuses them andNumber()rounds above 2^53. Convert withString()at the boundary of your code. - Read masks are part of the request, not an optimisation. The server checks every path and answers
invalid read_mask pathfor a typo, so a mistake surfaces at the first call instead of as silently empty fields; a missing mask returns a compact default and a broad mask on a stream is the difference between 30 MB and 66 GB a day. Write the mask first, then the handler. - History has a known floor. The full node held about ten days of checkpoints on the day we measured and
GetServiceInfotells you the exact lowest one. Requests below it come back with a clear error rather than quietly routing to an archive, so a backfill always knows where the window starts. Design it againstlowest_available_checkpoint, not against an assumption. - Subscriptions start at the tip. Nothing is replayed on resubscribe. Persist the last sequence number you processed and backfill the gap, as in the code above; Mysten’s guide says the same and adds: keep reading the subscription while the backfill runs, so the buffer never grows unbounded.
- Pagination uses opaque tokens and filters use typed shapes.
ListEvents,ListOwnedObjectsand friends return anext_page_tokenyou pass back verbatim. The JSON-RPC cursor objects do not carry over. Negative filters (MatchNone) behave differently, so port each query rather than translating cursors.
One more that is not a gotcha but a habit: a public load balancer URL is not a production endpoint, Mysten says so in its migration guide. Rate limits and retention there are whatever the Foundation sets that week. A dedicated endpoint gives you a fixed retention window you can query, a stream quota you know and a bill you can compute from the table above.
Why BoltRPC for Sui gRPC
The gRPC door on BoltRPC is the same endpoint and the same key you already use for Sui JSON-RPC, so the migration touches your client code and nothing in your account.
- Native gRPC and gRPC-Web on one key. HTTP/2 on
endpoints.boltrpc.io:443for the Rust, Go and Python clients,https://endpoints.boltrpc.io/grpc/suifor the TypeScript SDK and for browsers, withx-api-keymetadata on both. No proxy, no library shims. - Priced like JSON-RPC. A unary gRPC call is charged like the matching JSON-RPC method, so the migration does not change your bill for reads.
BatchGetObjectsandBatchGetTransactionsbill once whatever the item count. - Streams billed by bytes. Subscriptions and list calls are charged on what they deliver, not on how often they tick. The number of streams a key may hold open at once is listed with the plans. Your read mask is your price lever, as the measured table shows.
- A retention window you can query.
GetServiceInforeturns the oldest checkpoint the node holds, so a backfill never guesses. - Both Move chains behind one key. Sui and Aptos sit on the same key and the same dashboard, so a team that runs both keeps one integration instead of two.
- The endpoint is measured, continuously. Per-chain round-trip latency is sampled every second and published on the networks page; the numbers in this guide came from the same gateway your traffic uses.
The network page shows gRPC with a Beta badge: the API is young, Mysten is still extending it and the endpoint tracks each release. Mysten itself has called the gRPC API generally available.
FAQ
Is Sui JSON-RPC deprecated?
Yes. The Sui Foundation disabled JSON-RPC on its Mainnet full nodes in the week of 27 July 2026 and plans full decommission, including removal from the node code, for mid-October 2026. The replacements are gRPC (sui.rpc.v2) for applications and indexers, or GraphQL for filtered queries without streaming. The TypeScript SDK marks SuiJsonRpcClient deprecated in favour of SuiGrpcClient.
Does gRPC cost more than JSON-RPC on BoltRPC?
No. A unary gRPC call is charged like the matching JSON-RPC method, so GetObject costs what sui_getObject costs. Batch calls bill once whatever the item count and streams are charged on delivered data rather than per message, which makes a minimal checkpoint feed far cheaper than polling once a second. The pricing page has the exact rates.
Does gRPC work over HTTP and in the browser?
gRPC runs over HTTP/2. Browsers cannot open raw HTTP/2 gRPC connections, so BoltRPC also serves gRPC-Web at https://endpoints.boltrpc.io/grpc/sui, which works from a browser and is what the @mysten/sui TypeScript client uses. Native clients in Rust, Go or Python connect to endpoints.boltrpc.io:443 directly.
Is gRPC stateful or stateless?
Unary gRPC calls are stateless: each request carries everything the server needs, like a JSON-RPC call. Server streams keep a connection open and push messages, but the server holds no position for you: a Sui subscription starts at the tip when opened and does not replay, so your application keeps the last processed sequence number and backfills gaps itself.
How far back does Sui gRPC history go?
A full node keeps a rolling window of recent checkpoints and reports the oldest one in GetServiceInfo as lowest_available_checkpoint. On the BoltRPC endpoint that window was about ten days on 30 September 2026. That window is what a live application needs. GetServiceInfo tells you exactly where it starts, so a backfill never has to guess. Older history lives in Mysten’s archival store.
Related
- Sui RPC endpoint: URLs, connect snippets, the gRPC door
- WebSocket vs polling, measured: the polling and WebSocket numbers behind the stream comparison
- gRPC vs JSON-RPC: the protocol comparison
- How RPC request units work
- RPC rate limits explained
- Aptos RPC endpoint: the other Move chain on the same key
Start migrating
The code in this guide runs unchanged against https://endpoints.boltrpc.io/grpc/sui with your key in the x-api-key header. Create an API key, swap the client and move the streams first: that is where the shutdown bites and where the savings are.
