Pagination with Google Sheets API: limit, offset, and page numbers
How to implement pagination when reading Google Sheets data through SheetsAPI — using limit and offset parameters with real code examples.
Why pagination matters for Sheets data
A Google Sheet with a few hundred rows loads fine in a browser. A sheet with 10,000 rows is a different story — fetching everything at once is slow, burns bandwidth, and makes your UI render a wall of data nobody asked for. Pagination solves this by fetching a small slice at a time.
GKit SheetsAPI has first-class pagination support through two query parameters: limit and offset. Every list response also returns a meta object that tells you the total row count, so you can calculate pages and render navigation without a separate count request.
The limit and offset parameters
| Parameter | Default | Max | Description |
|---|---|---|---|
limit | 100 | 1000 | Number of rows to return |
offset | 0 | — | Number of rows to skip (0-based) |
A basic paginated request looks like this:
GET https://sheetsapi.gkit.mreshank.com/api/spreadsheets/{userKey}/{sheet}?limit=20&offset=40
That returns rows 41–60 (skip the first 40, return the next 20).
The response always includes a meta block:
{
"data": [...],
"meta": {
"total": 243,
"limit": 20,
"offset": 40
}
}meta.total is the full row count of the sheet — not just the current page. Use it to calculate total pages and decide whether a "next page" button should be shown.
Calculating total pages
function totalPages(total: number, limit: number): number {
return Math.ceil(total / limit);
}
// total: 243, limit: 20 → 13 pagesTo fetch a specific page number (1-based):
const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
async function fetchPage(
userKey: string,
sheet: string,
page: number,
pageSize = 20
) {
const offset = (page - 1) * pageSize;
const url = new URL(`${BASE}/${userKey}/${sheet}`);
url.searchParams.set("limit", String(pageSize));
url.searchParams.set("offset", String(offset));
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
});
return res.json() as Promise<{
data: Record<string, string>[];
meta: { total: number; limit: number; offset: number };
}>;
}Building a page-number UI in React
import { useState, useEffect } from "react";
const PAGE_SIZE = 20;
export function PaginatedTable({ userKey, sheet }: { userKey: string; sheet: string }) {
const [page, setPage] = useState(1);
const [rows, setRows] = useState<Record<string, string>[]>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
fetchPage(userKey, sheet, page, PAGE_SIZE).then(({ data, meta }) => {
setRows(data);
setTotal(meta.total);
});
}, [userKey, sheet, page]);
const pages = Math.ceil(total / PAGE_SIZE);
return (
<div>
<table>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{Object.values(row).map((cell, j) => <td key={j}>{cell}</td>)}
</tr>
))}
</tbody>
</table>
<nav>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>
Previous
</button>
<span>Page {page} of {pages}</span>
<button onClick={() => setPage(p => Math.min(pages, p + 1))} disabled={page === pages}>
Next
</button>
</nav>
</div>
);
}Infinite scroll with offset accumulation
For a feed-style UI, accumulate rows instead of replacing them. Track the current offset and append each new page to the existing list:
const [rows, setRows] = useState<Record<string, string>[]>([]);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(true);
async function loadMore() {
const { data, meta } = await fetchPage(userKey, sheet, 1, PAGE_SIZE);
// use raw offset instead
const url = new URL(`${BASE}/${userKey}/${sheet}`);
url.searchParams.set("limit", String(PAGE_SIZE));
url.searchParams.set("offset", String(offset));
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${process.env.GKIT_API_KEY}` },
});
const json = await res.json();
setRows(prev => [...prev, ...json.data]);
setOffset(prev => prev + PAGE_SIZE);
setHasMore(offset + PAGE_SIZE < json.meta.total);
}Wire loadMore to an Intersection Observer on a sentinel element at the bottom of the list and you have a fully functional infinite scroll — no external library needed.
A note on limit max
The maximum limit is 1000. If your sheet has fewer than 1000 rows and you want everything in one request, set limit=1000. For sheets larger than 1000 rows you must paginate — there is no way to fetch all rows in a single call.