Skip to main content

Cursors

A server-side cursor lets you stream a large result set a batch at a time instead of loading every row into memory at once. Opt in by passing cursor: true to connection.query():

const result = await connection.query('select * from customers order by id', {
objectRows: true,
cursor: true,
});

const cursor = result.cursor!;

result.rows is not populated when cursor: true — rows are fetched on demand through the returned Cursor instead.

Reading rows

cursor.next() returns one row at a time (or undefined once exhausted):

let row;
while ((row = await cursor.next())) {
console.log(row);
}

cursor.fetch(n) reads up to n rows in one call, returning fewer than n once the result set runs out:

const rows = await cursor.fetch(50); // up to 50 rows

cursor.rowType reports 'array' or 'object', matching the objectRows option the query was opened with.

Batch size: fetchCount

fetchCount controls how many rows the cursor asks the server for per internal round trip (its execute-portal batch size), independent of how many rows you request from next()/fetch():

const result = await connection.query('select * from customers', {
cursor: true,
fetchCount: 500, // fetch 500 rows per round trip from the server
});

It defaults to 100 and must be between 0 and 4294967295.

Events

A cursor emits:

  • 'fetch' — whenever a new batch is pulled from the server, with the batch of rows.
  • 'close' — when the cursor closes, whether explicitly or because the result set was exhausted.
cursor.on('fetch', rows => console.log(`fetched ${rows.length} rows`));
cursor.on('close', () => console.log('cursor closed'));

A cursor closes itself automatically once next()/fetch() reaches the end of the result set.

Closing

Always close a cursor you don't drain to completion, to release the underlying portal:

await cursor.close();

Cursor implements AsyncDisposable, so await using closes it automatically at the end of a block:

{
const result = await connection.query('select * from customers', { cursor: true });
await using cursor = result.cursor!;
const row = await cursor.next();
// cursor.close() runs automatically here
}

See Resource Management for more on await using and disposal in postgrejs.

Why use a cursor

Without cursor: true, query() buffers the entire result set in memory before resolving. For queries returning millions of rows, that defeats streaming and can exhaust memory. A cursor keeps only fetchCount rows in memory at a time, fetching more from the server as you consume them.

See also