Sui Move events are emitted with sui::event::emit, carry a fully-qualified type, and are indexed by fullnodes so they can be queried with suix_queryEvents using an EventFilter and a cursor. The EventFilter union includes All, Transaction, MoveModule, MoveEventType, MoveEventField, Sender, TimeRange, and Package, each with distinct matching semantics. Pagination is cursor-based only: the response returns data[] plus nextCursor and hasNextPage, and you walk pages by passing the previous nextCursor with a limit and an order. A reliable indexer stores the last processed cursor (or txDigest and eventSeq), polls from that cursor, and reconciles missed events after a disconnect, while respecting the fullnode's retention window.
What a Sui Move event is and how it is stored
A Sui Move event is a structured record emitted during transaction execution with sui::event::emit. Each event carries a fully-qualified type such as 0x2::sui::SUI or a package::module::Struct path, and the fullnode indexes it so clients can query it later. The authoritative description of emission and indexing is in the Sui documentation's Emitting Events page.
When you query events, the RPC returns each event with a global event id, the transaction digest and sequence number, the type string, a parsedJson rendering, and a bcs(base64) canonical payload. The parsedJson is convenient for application logic, but the bcs field is the canonical representation you should use when you need to verify or re-serialize the event exactly as emitted.
Events are indexed by the fullnode, not stored forever. Older events may fall outside the node's retention window, which is why historical event queries often require an archive or a dedicated indexer. OnFinality's Sui archive nodes and historical RPC page explains how archive access extends the queryable history beyond a standard fullnode.
- Emitted with sui::event::emit during transaction execution.
- Carries a fully-qualified type: package::module::Struct.
- Returned with id, txDigest, sequence, type, parsedJson, and bcs(base64).
- Indexed by the fullnode; retention is limited and varies by node.
The suix_queryEvents method and its parameters
The suix_queryEvents method is the JSON-RPC entry point for reading indexed events. It accepts a query object containing an EventFilter, a limit, a cursor, and an order of Ascending or Descending. The response is a QueryEventsResult with a data array and a nextCursor, plus a hasNextPage flag in current implementations. The method and its types are documented in the Sui API Reference and the sui_json_rpc crate documentation.
The limit controls how many events are returned per page. The cursor is an opaque token from the previous page's nextCursor; you should treat it as a black box and never construct or parse it yourself. The order determines whether the page walks forward or backward through the indexed event stream.
Because the method is part of the legacy JSON-RPC surface, some providers mark it as deprecated in favor of newer event APIs. The Sui JSON-RPC migration documentation tracks this transition. If your endpoint reports deprecation, verify the current recommended event API for your use case before building a long-lived integration.
- query: { filter, limit, cursor, order }.
- Response: { data[], nextCursor, hasNextPage }.
- order is Ascending or Descending.
- Cursor is opaque; always reuse the returned nextCursor.
EventFilter variants and their exact matching semantics
The EventFilter union defines how events are selected. All matches every event. Transaction matches events from a specific transaction digest. MoveModule matches events from a package and module pair. MoveEventType matches a fully-qualified struct type string. MoveEventField matches a parsed field path and value. Sender matches events emitted by a specific address. TimeRange matches events within a timestamp range. Package matches events from a package.
The distinction between StructType and MoveEventType matters. StructType matching compares the event's struct type, while MoveEventType matching compares the event's type tag. In practice, this means the string you pass must be the fully-qualified type exactly as emitted, including the leading 0x address and the module and struct names. A partial or unqualified string will silently return no results.
MoveEventField filters compare parsed values, so numbers and ids must match the parsed representation. If you filter on a field that is a string in parsedJson, pass a string; if it is a number, pass a number. Mismatched types are a common cause of empty result sets.
- All: no filtering.
- Transaction: by transaction digest.
- MoveModule: by package and module.
- MoveEventType: by fully-qualified struct type string.
- MoveEventField: by parsed field path and value.
- Sender: by emitting address.
- TimeRange: by timestamp range.
- Package: by package address.
Cursor pagination: the only correct way to page
Sui event queries do not support offset pagination. You page by passing the previous response's nextCursor into the next request. This is the only correct way to walk the event stream without skipping or duplicating events, because the underlying index can change between requests and offsets would drift.
Descending order plus a cursor is the standard way to page backward through history. If you want the most recent events first, set order to Descending and start with an empty cursor. Each subsequent request uses the nextCursor from the previous page. When hasNextPage is false or nextCursor is null, you have reached the end of the available history for that filter.
A common mistake is to reuse the same cursor across different filters or to ignore the cursor entirely and re-query from the start. Both cause loops or duplicate processing. Always store the cursor alongside the filter that produced it, and only reuse it with the same filter and order.
- No offset pagination; use nextCursor only.
- Descending order walks backward through history.
- hasNextPage false or nextCursor null means end of history.
- Store the cursor with its filter and order to avoid loops.
A runnable Node.js example: filter by MoveEventType and page with cursors
The following Node.js example calls suix_queryEvents with a MoveEventType filter for a specific struct, then loops on nextCursor to collect a fixed number of pages. It prints the key fields of each event so you can see the shape of the response. Replace the endpoint URL and the struct type with your own values.
The example uses fetch, which is available in modern Node.js. It stores the cursor between iterations and stops when there is no next page or when the page limit is reached. This is the same pattern you would use in a production indexer, with the addition of persistence for the cursor.
const ENDPOINT = 'https://your-sui-rpc-endpoint';
const STRUCT_TYPE = '0x2::sui::SUI';
const PAGE_LIMIT = 50;
const MAX_PAGES = 5;
async function queryEventsPage(cursor) {
const body = {
jsonrpc: '2.0',
id: 1,
method: 'suix_queryEvents',
params: [
{
MoveEventType: STRUCT_TYPE
},
cursor,
PAGE_LIMIT,
true // descending
]
};
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
async function collectPages() {
let cursor = null;
let pages = 0;
const all = [];
while (pages < MAX_PAGES) {
const result = await queryEventsPage(cursor);
for (const event of result.data) {
all.push(event);
console.log({
id: event.id,
txDigest: event.id.txDigest,
sequence: event.id.eventSeq,
type: event.type,
parsedJson: event.parsedJson,
bcs: event.bcs
});
}
pages += 1;
if (!result.hasNextPage || !result.nextCursor) break;
cursor = result.nextCursor;
}
console.log('collected', all.length, 'events across', pages, 'pages');
}
collectPages().catch(console.error);Filtering by sender and by transaction digest
Filtering by sender is useful when you want all events emitted by a specific address, regardless of type. The filter is Sender with the address string. This is common for wallet activity feeds and for auditing a specific account's interactions with multiple packages.
Filtering by transaction digest is useful when you already know the transaction and want to inspect its events. The filter is Transaction with the digest string. This is the most precise filter and returns only the events from that single transaction, in sequence order.
Both filters support the same cursor pagination. If a sender has a very large event history, you will page through it with nextCursor just like any other filter. The retention window still applies, so very old sender events may not be available on a standard fullnode.
- Sender: all events from an address.
- Transaction: all events from one transaction digest.
- Same cursor pagination and retention rules apply.
Polling pattern for a continuous indexer
A continuous indexer should keep the last processed cursor, or the last processed (txDigest, eventSeq) pair, and poll queryEvents from that cursor on an interval. This ensures that each poll resumes exactly where the previous one stopped, without gaps or duplicates. If you prefer realtime delivery, the WebSocket subscription described in Sui WebSocket event subscriptions can push events as they are emitted, but you should still store a cursor so you can reconcile after a disconnect.
After a disconnect, re-query from the stored cursor to fetch any events that were emitted while you were offline. This reconciliation step is what makes the indexer reliable. Without it, a dropped WebSocket connection silently loses events.
For high-volume indexing, consider batching your queries and using a dedicated endpoint. OnFinality's API service and RPC pricing pages describe the available plans and how to size them for sustained event polling.
- Store the last cursor or (txDigest, eventSeq).
- Poll from that cursor on an interval.
- Use WebSocket for realtime, but keep a cursor for reconciliation.
- Re-query from the stored cursor after a disconnect.
Results table: measure your endpoint's event query limits
Event query behavior varies by provider and node configuration. Use the table below to record what your own endpoint reports. Fill in each row by running a query and observing the response, or by checking your provider's documentation. Do not assume values from another provider apply to yours.
For the max limit per page, try a large limit and see whether the endpoint caps it or returns an error. For the retention window, query for an event you know is old and see whether it is returned. For deprecation, check whether the endpoint returns a deprecation notice or whether the provider documents a replacement API.
- Max limit per page: [your value]
- Retention window: [your value]
- Deprecated in favor of new event API: [yes/no]
- Cursor stability across requests: [your observation]
- Rate limit for event queries: [your value]
Common failures and how to diagnose them
Empty results are the most common failure. The first thing to check is whether the filter type is the fully-qualified struct type. A missing 0x prefix, a wrong module name, or a partial type string will return nothing. Verify the exact type string from the event's type field in a known transaction.
Events older than the node's retention window will also return nothing. If you are querying history and getting empty pages, check whether the events are within the retention window. If they are not, you need an archive or a dedicated indexer. The Sui archive nodes and historical RPC page covers this.
Cursor misuse causes loops or duplicates. If you reuse a cursor with a different filter, or if you ignore the cursor and re-query from the start, you will process the same events repeatedly. Always store the cursor with its filter and order, and only reuse it with the same query.
Relying on the deprecated JSON-RPC path instead of the current event API can cause failures if the endpoint has removed or restricted the method. Check your provider's documentation and the Sui JSON-RPC migration notes before building a long-lived integration.
- Empty results: verify the fully-qualified struct type.
- Empty results: check the retention window.
- Loops or duplicates: store and reuse the cursor correctly.
- Deprecation: verify the current event API for your endpoint.
Limitations and tradeoffs
Event queries are limited by the fullnode's retention window. A standard fullnode does not store all history, so very old events require an archive or an external indexer. This is a fundamental tradeoff between storage cost and queryability.
Cursor pagination is reliable but not random-access. You cannot jump to an arbitrary offset; you must walk from a known cursor. For large histories, this means your first query may take many pages before you reach the events you want.
The parsedJson rendering is convenient but not canonical. If you need to verify or re-serialize an event exactly, use the bcs field. The parsedJson may change format across versions, so do not depend on its exact shape for long-term storage.
Event query rate limits and page size limits vary by provider. OnFinality does not publish specific event query latency or throughput numbers because they depend on the endpoint and workload. Measure against your own endpoint using the results table above.
- Retention window limits historical queries.
- Cursor pagination is not random-access.
- parsedJson is not canonical; use bcs for verification.
- Rate and page limits vary by provider.
Troubleshooting checklist
Use this checklist when an event query does not behave as expected. Work through it in order, because the most common causes are also the easiest to check.
If you are new to Sui RPC, the Sui RPC guide (RPC Assistant) provides a broader orientation. For object reads and dynamic fields, see Reading Sui objects: getObject, dynamic fields, and pagination. For transaction simulation, see Simulating Sui transactions with devInspectTransaction.
- Is the filter type fully qualified with 0x prefix?
- Is the event within the node's retention window?
- Are you passing the nextCursor from the previous page?
- Are you using the same filter and order with the cursor?
- Is the method deprecated on your endpoint?
- Are you hitting a rate limit or page size limit?
- Are you reading parsedJson when you should read bcs?
Next steps and further reading
To go deeper, start with the OnFinality Learn hub for related Sui guides, and the Sui RPC guide (RPC Assistant) for a method-by-method orientation. If you need historical events beyond a fullnode's retention window, review Sui archive nodes and historical RPC. For realtime delivery, see Sui WebSocket event subscriptions.
When you are ready to run event queries in production, compare plans on the RPC pricing page and review the API service for managed endpoints. For network-specific details, see the Sui network page.
The authoritative primary sources for the claims in this article are the Sui documentation's Emitting Events page and the Sui API Reference for suix_queryEvents. Always verify method behavior against your own endpoint, because provider implementations and retention windows differ.
- Review the OnFinality Learn hub for related guides.
- Check archive nodes for historical events.
- Use WebSocket subscriptions for realtime delivery.
- Verify behavior against your own endpoint.