Row limits and pagination
Why does SuiteQL OFFSET return the same rows as page 1?
Short answer
The SQL clause OFFSET n ROWS FETCH NEXT m ROWS ONLY has its OFFSET silently discarded on both engines, even with an explicit ORDER BY. Every page returns the first rows again. Paginate with the REST ?offset= parameter instead.
Because the OFFSET is thrown away. The SQL clause OFFSET n ROWS FETCH NEXT m ROWS ONLY parses, runs and returns rows, but the offset has no effect on either engine, with or without ORDER BY. Page through the REST ?offset= URL parameter instead.
Minimal reproduction
Two statements, run one after the other through N/query.runSuiteQL:
SELECT id FROM transaction ORDER BY id OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
SELECT id FROM transaction ORDER BY id OFFSET 500 ROWS FETCH NEXT 5 ROWS ONLY
Both returned the same five ids in the same order, with no error. OFFSET 5 behaves the same as OFFSET 500, so the size of the offset is not the variable. Sent to the REST endpoint at ?limit=5, those same two statements returned the same five ids again, HTTP 200. The same thing happens at OFFSET 5 with no ORDER BY, and at OFFSET 10 both unordered and ordered, on repeated back to back runs.
On the two REST calls the envelope comes back { count: 5, hasMore: false, offset: 0, totalResults: 5 }. That envelope is not itself the bug: FETCH NEXT 5 ROWS ONLY bounds the whole result set to five rows, so a five-row result set is what the envelope is describing. A further call, the same ?limit=5 with the FETCH clause removed from the SQL, comes back hasMore: true. The trap is still real, because a caller paging by incrementing the SQL OFFSET sees every field in the response agreeing that the result set was exhausted, and stops after page 1. What totalResults reports when the SQL does not cap the result set is a separate trap, covered on the totalResults page.
The fix
On the REST endpoint, move the paging out of the SQL and into the URL:
POST /services/rest/query/v1/suiteql?limit=5&offset=500
body: {"q":"SELECT id FROM transaction ORDER BY id"}
That returns the 501st through 505th rows of that ordering and the envelope echoes offset: 500. That was checked once. Note there is no FETCH clause in the SQL now, so limit alone decides the page size. limit is capped at 1000 server side; 1001 returns HTTP 400 with The specified query parameter 'limit' is out of bounds. Provide value between 1 and 1000. Offset is a row ordinal, not an id: with ORDER BY id, offset 500 lands on the 501st id that exists, which is nowhere near id 501, because internal ids are sparse.
Through N/query.runSuiteQL there is no URL parameter to fall back on, and the SQL clause is all there is. It does not work, so you cannot get past the first page this way at all. The N/query module's own paged-results API is the intended mechanism there; this run only establishes that the SQL clause is not it.
Why this happens
The clause is accepted by the parser and then dropped somewhere between parsing and execution, since FETCH NEXT m is honoured while OFFSET n from the same clause is not. It reproduces identically on the raw REST endpoint, so it is not an artifact of any one connector or bridge. Why the offset is discarded, and whether any SuiteQL version honours it, is not established by this run.
One thing this run did not find: instability. Paging with the REST ?offset= parameter and no ORDER BY, two identical page-1 calls returned the same ids in the same order, page 1 and page 2 had zero overlap, and their union was 20 distinct rows. That is a refutation of the usual advice, and a narrow one: one table, a small offset, an idle account, three calls. It says nothing about offset 50000 or about paging under concurrent writes. It also does not license dropping ORDER BY: without one there is still no guaranteed order between calls, and all this probe shows is that the engine did not volunteer an inconsistency on its own.
How to tell if you are affected
Pick a table you know holds more than ten rows, so that a second page of five exists to be compared. Run these two and compare the first id of each:
SELECT id FROM transaction ORDER BY id OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
SELECT id FROM transaction ORDER BY id OFFSET 5 ROWS FETCH NEXT 5 ROWS ONLY
Same first id, and the same five ids throughout, means your engine discards the offset, and any loop you have built on this clause has been re-reading page 1. If the second statement starts at the sixth id and shares nothing with the first, the clause works for you and this page does not apply. The ORDER BY matters: without it, matching rows could be explained away as scan order, and with it they cannot.
Scope: one production account on NetSuite 2026.1, Administrator role. The discarded SQL OFFSET was reproduced on both N/query.runSuiteQL and the REST endpoint. The working ?offset= parameter, the envelope readings and the stability check are REST only; N/query.runSuiteQL returns no envelope and has no such parameter.
How this was established
11 probe runs against one live NetSuite 2026.1 account on the Administrator role, through N/query.runSuiteQL and cross-checked against the REST query endpoint. Where this page draws a boundary around a finding, that boundary is the edge of what was actually run.
Related
- Why does totalResults in the SuiteQL REST response not match SELECT COUNT(*)?
totalResults is not a row count. It reports min(rows your query returns, limit x 1000), so on a larger result set it reports the cap and calls it the total, HTTP 200, no flag. Run SELECT COUNT(*) separately.
- Why does LIMIT 5 not work in SuiteQL, and what should I use instead?
LIMIT is not in the SuiteQL dialect. Both engines reject it with a parse error pointing at the number after it. Use FETCH FIRST 5 ROWS ONLY; WHERE ROWNUM <= 5 and SELECT TOP 5 also worked.
From the blog
- SuiteScript Governance: Why Your Script Works in Sandbox and Dies in Production
Governance units are a per-execution budget, not a rate limit. The arithmetic that decides whether your SuiteScript survives, and the four substitutions that buy back the most headroom.