{"id":1003,"date":"2025-11-21T13:59:25","date_gmt":"2025-11-21T21:59:25","guid":{"rendered":"https:\/\/embedded.gusto.com\/blog\/?p=1003"},"modified":"2025-11-20T13:59:39","modified_gmt":"2025-11-20T21:59:39","slug":"api-pagination","status":"publish","type":"post","link":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/","title":{"rendered":"A Developer&#8217;s Guide to API Pagination: Offset vs. Cursor-Based"},"content":{"rendered":"<h1 id=\"a-developers-guide-to-api-pagination-offset-vs.-cursor-based\">A Developer\u2019s Guide to API Pagination: Offset vs. Cursor-Based<\/h1>\n<p>You\u2019ve just built a shiny new feature that lists user transactions. In testing, it runs flawlessly: fast responses, clean UI, no complaints. Then your biggest client joins with 50,000 records. Suddenly, that endpoint stalls and times out, and support tickets start piling up.<\/p>\n<p>This is a classic pagination problem. Instead of trying to load an entire data set at once, pagination breaks it into smaller, more manageable chunks (or pages) that can be fetched incrementally. Your bank does this; it shows around ten recent transactions at a time, not your entire account history. There\u2019s a good reason for this: research shows that even a one-second delay in page load can <a href=\"https:\/\/www.forbes.com\/sites\/rogerdooley\/2012\/12\/04\/fast-sites\/#:~:text=7%25%20loss%20in%20conversions\">reduce conversions by 7 percent<\/a>. When you\u2019re dealing with payroll systems, financial data, or anything time sensitive, those delays translate to missed deadlines, compliance headaches, and frustrated customers.<\/p>\n<p>In this article, you\u2019ll explore two common approaches to pagination: <em>offset-based<\/em> and <em>cursor-based<\/em>. You\u2019ll learn the trade-offs of each and how to implement pagination in the real world using the <a href=\"https:\/\/docs.gusto.com\/embedded-payroll\/docs\/introduction\">Gusto Embedded Payroll API<\/a>.<\/p>\n<h2 id=\"compare-offset-vs.-cursor-based-api-pagination-which-method-to-choose\">Compare Offset vs. Cursor-Based API Pagination: Which Method to Choose<\/h2>\n<p>API pagination controls how much data flows between a client and a server in each request. Instead of returning an entire data set at once, the API sends back a smaller subset, along with details on how to fetch the next one. These details can include the total number of items, page numbers, or a cursor marking where to continue. By fetching data in steps, pagination keeps applications fast and responsive while preventing large data sets from overloading the server or client.<\/p>\n<h3 id=\"understand-offset-pagination\">Understand Offset Pagination<\/h3>\n<p>Offset pagination is one of the simplest and most common ways to paginate API results. You specify an <em>offset<\/em> (how many records to skip from the start of the data set) and a <em>limit<\/em> (how many records to return) while fetching data. This method works much like saying, \u201cSkip the first fifty records and show me the next twenty.\u201d It\u2019s easy to understand and implement, which is why many beginner-friendly APIs and SQL queries use it.<\/p>\n<h4 id=\"learn-how-offset-pagination-works\">Learn How Offset Pagination Works<\/h4>\n<p>The client specifies two parameters: <code class=\"\" data-line=\"\">offset<\/code> and <code class=\"\" data-line=\"\">limit<\/code> (or <code class=\"\" data-line=\"\">page_size<\/code>). Here\u2019s what a typical API request looks like:<\/p>\n<pre class=\" language-bash\"><code class=\"\" data-line=\"\">\nGET \/api\/transactions?limit&lt;span class=&quot;token operator&quot;&gt;=&lt;\/span&gt;20&lt;span class=&quot;token operator&quot;&gt;&amp;&lt;\/span&gt;offset&lt;span class=&quot;token operator&quot;&gt;=&lt;\/span&gt;40\n\n<\/code><\/pre>\n<p>This translates to \u201cskip the first forty records and give me the next twenty,\u201d essentially fetching page 3 if each page contains twenty items. On the backend, this maps directly to an SQL query:<\/p>\n<pre class=\" language-sql\"><code class=\"\" data-line=\"\">\n```sql\n-- Fetch 20 records starting from the 41st record\nSELECT * FROM transactions\nORDER BY created_at DESC\nLIMIT 20 OFFSET 40;\n```\n<\/code><\/pre>\n<p>The response typically includes metadata to help clients navigate:<\/p>\n<pre class=\" language-sql\"><code class=\"\" data-line=\"\">\n```json\n{\n  &quot;data&quot;: [...],\n  &quot;pagination&quot;: {\n    &quot;limit&quot;: 20,\n    &quot;offset&quot;: 40,\n    &quot;total&quot;: 5000\n  }\n}\n```\n\n<\/code><\/pre>\n<p>With the total count, clients can calculate exactly how many pages exist and build traditional page number navigation (page 1, 2, 3 \u2026 250).<\/p>\n<h4 id=\"examine-the-benefits-and-drawbacks-of-offset-pagination\">Examine the Benefits and Drawbacks of Offset Pagination<\/h4>\n<p>Offset pagination is simple and easy to implement. Most object-relational mappings (ORMs) and database libraries support <code class=\"\" data-line=\"\">LIMIT<\/code>\/<code class=\"\" data-line=\"\">OFFSET<\/code> out of the box, and the math involved is intuitive: <code class=\"\" data-line=\"\">offset = (page_number - 1) * page_size<\/code>. Users can jump to any page, bookmark specific pages, or navigate backward and forward freely. For small to medium data sets (a few thousand records), performance is good (queries produce results quickly), and the user experience matches what people expect from traditional web pagination.<\/p>\n<p>The performance problems for offset pagination <a href=\"%5Bhttps:\/\/stackoverflow.com\/questions\/70661360\/pagination-getting-slower-while-page-number-increasing%5D(https:\/\/stackoverflow.com\/questions\/70661360\/pagination-getting-slower-while-page-number-increasing)\">show up at scale<\/a>. As the offset grows, so does the query cost. A request like <code class=\"\" data-line=\"\">OFFSET 10000<\/code> forces the database to scan and discard 10,000 rows before returning results. Fetching page 1 can take 10 milliseconds, while page 1000 can take several seconds on the same data set.<\/p>\n<p>There\u2019s also the shifting data problem. Say a user is on page 5 of transaction records. While they\u2019re browsing, three new transactions are added at the top. When they click <strong>Next<\/strong>, the offset advances, but so does the data. Now they either see duplicate records or skip some entirely (phantom records). This shifting-records issue makes offset pagination unreliable for real-time or frequently changing data.<\/p>\n<p>Then there\u2019s the total issue. Running <code class=\"\" data-line=\"\">SELECT COUNT(*) FROM transactions<\/code> adds more overhead. On large tables, counting millions of rows is expensive and only gets slower as data grows. Some APIs skip this entirely and lose the ability to show total page numbers, while others cache the count and accept stale numbers.<\/p>\n<h4 id=\"know-when-to-use-offset-pagination\">Know When to Use Offset Pagination<\/h4>\n<p>Despite its limitations, offset pagination works well for specific use cases: admin dashboards with mostly static data, search results where users rarely go past the first few pages, or any small data set (under ~10,000 records). It\u2019s also helpful when users expect traditional page number navigation or need to share links to specific pages.<\/p>\n<p>For large, fast-changing data sets or high-traffic applications where speed matters, cursor-based pagination is usually a better fit.<\/p>\n<h3 id=\"understand-cursor-based-pagination\">Understand Cursor-Based Pagination<\/h3>\n<p>Instead of counting rows from the beginning each time, cursor-based pagination (also called keyset pagination) uses a pointer (the cursor) that marks your current position in the data set. This cursor is like a bookmark pointing to a specific row in your data set.<\/p>\n<h4 id=\"learn-how-cursor-based-pagination-works\">Learn How Cursor-Based Pagination Works<\/h4>\n<p>Rather than asking, \u201cskip forty records and give me twenty,\u201d cursor pagination specifies, \u201cgive me twenty records starting after this specific marker.\u201d The cursor is typically an encoded reference to the last item you received: often a combination of the record\u2019s ID and timestamp, or a unique identifier that the database can use to locate the next batch.<\/p>\n<p>Here\u2019s what a typical API request looks like:<\/p>\n<pre class=\" language-bash\"><code class=\"\" data-line=\"\">\n```bash\nGET \/api\/transactions?limit=20&amp;cursor=eyJpZCI6MTIzNDUsInRzIjoiMjAyNC0wMS0xNVQxMDowMDowMFoifQ==\n```\n<\/code><\/pre>\n<p>The cursor value is usually <a href=\"https:\/\/en.wikipedia.org\/wiki\/Base64\">Base64-encoded<\/a> to obscure internal implementation details and prevent clients from manually constructing invalid cursors. On the backend, this translates to a query like this:<\/p>\n<pre class=\" language-sql\"><code class=\"\" data-line=\"\">\n```sql\n-- Fetch 20 records after the given marker\nSELECT * FROM transactions\nWHERE (created_at, id) &lt; (&#039;2025-10-15 10:00:00&#039;, 12345)\nORDER BY created_at DESC, id DESC\nLIMIT 20;\n```\n<\/code><\/pre>\n<p>In this <code class=\"\" data-line=\"\">WHERE<\/code> clause, instead of skipping rows with <code class=\"\" data-line=\"\">OFFSET<\/code>, you use a filter condition that the database can optimize with appropriate indexes. The response may look like this:<\/p>\n<pre class=\" language-json\"><code class=\"\" data-line=\"\">\n```json\n{\n  &quot;data&quot;: [\n    {\n      &quot;id&quot;: 12344,\n      &quot;amount&quot;: 1500.00,\n      &quot;created_at&quot;: &quot;2025-10-15T09:58:30Z&quot;\n    },\n    \/\/ ... 19 more records\n  ],\n  &quot;pagination&quot;: {\n    &quot;next_cursor&quot;: &quot;eyJpZCI6MTIzMjUsInRzIjoiMjAyNC0wMS0xNVQwODowMDowMFoifQ==&quot;,\n    &quot;prev_cursor&quot;: &quot;eyJpZCI6MTIzNDQsInRzIjoiMjAyNC0wMS0xNVQwOTo1ODozMFoifQ==&quot;,\n    &quot;has_more&quot;: true\n  }\n}\n```\n<\/code><\/pre>\n<p>Here, <code class=\"\" data-line=\"\">next_cursor<\/code> and <code class=\"\" data-line=\"\">prev_cursor<\/code> act as pointers to navigate the records.<\/p>\n<h4 id=\"examine-the-benefits-and-drawbacks-of-cursor-based-pagination\">Examine the Benefits and Drawbacks of Cursor-Based Pagination<\/h4>\n<p>Cursor-based pagination delivers <a href=\"https:\/\/www.milanjovanovic.tech\/blog\/understanding-cursor-pagination-and-why-its-so-fast-deep-dive\">consistent performance<\/a> regardless of how deep you paginate. Whether you\u2019re fetching the first page or the ten-thousandth, the query cost remains constant (time complexity: <a href=\"https:\/\/www.geeksforgeeks.org\/dsa\/what-does-constant-time-complexity-or-big-o1-mean\/\">O(1)<\/a>) because you\u2019re always using indexed filters rather than scanning and discarding rows. This makes it ideal for large data sets where offset pagination can grind to a halt.<\/p>\n<p>Cursors also solve the shifting data problem. Cursors track specific records, not positions, so new entries don\u2019t cause duplicates or skipped items. If new transactions are inserted while a user is paginating, they don\u2019t see duplicates or skip records; the cursor maintains its position relative to the data itself, not relative to an arbitrary row count. This reliability makes cursor pagination ideal for real-time feeds, activity streams, or any constantly changing data set.<\/p>\n<p>From an architectural standpoint, cursors scale better as well. You don\u2019t need expensive <code class=\"\" data-line=\"\">COUNT(*)<\/code> queries for the total count. The database can efficiently use composite indexes on your sorting columns, and the queries remain fast even as your data set grows into millions of records.<\/p>\n<p>That said, cursor-based pagination adds complexity. You need to handle cursor encoding and decoding, build proper composite queries, and ensure your indexes support your filtering strategy. For developers new to API design, encoded cursor tokens are less intuitive than page numbers.<\/p>\n<p>Users also lose the ability to jump around. Navigation is typically forward (and sometimes backward), but skipping directly to page 50 isn\u2019t possible. This makes cursor pagination less practical when accessing random pages or when bookmarking specific pages is required. The UI often shifts to <strong>Load More<\/strong> buttons or infinite scroll.<\/p>\n<p>One more thing to consider is that cursors can become invalid if records are deleted or if your API enforces time-based expiration. This prevents users from fetching stale or invalid data, but it means APIs need to issue fresh cursors with each response and may return an error, prompting the client to restart pagination from the latest position.<\/p>\n<h4 id=\"know-when-to-use-cursor-based-pagination\">Know When to Use Cursor-Based Pagination<\/h4>\n<p>Cursor-based pagination is ideal for large, fast-changing data sets, like social media feeds, real-time events, chat histories, or any API where data is constantly added. Choose it when working on a project where consistent performance at scale matters more than random page access or when building mobile apps and infinite-scroll interfaces where users move sequentially through content.<\/p>\n<h2 id=\"analyze-a-real-world-example-gusto-embedded-api-pagination\">Analyze a Real-World Example: Gusto Embedded API Pagination<\/h2>\n<p>Gusto processes payroll and compliance data for thousands of companies, and relies on strategies that keep performance steady and data consistent, even with high-volume, real-time updates. Gusto Embedded is a great example of how production APIs can handle pagination at scale.<\/p>\n<h3 id=\"implement-the-dual-approach-of-gusto-embedded\">Implement the Dual Approach of Gusto Embedded<\/h3>\n<p>Gusto Embedded implements both <a href=\"https:\/\/docs.gusto.com\/embedded-payroll\/docs\/pagination\">offset-based and cursor-based pagination<\/a>, depending on the endpoint\u2019s characteristics. For most collection endpoints (like fetching employees), Gusto uses offset-based pagination:<\/p>\n<pre class=\" language-bash\"><code class=\"\" data-line=\"\">\n```bash\nGET https:\/\/api.gusto.com\/v1\/companies\/abc123\/employees?page=2&amp;per=5\n```\n\nPagination metadata is sent via HTTP headers:\n\n```\nX-Page: 2\nX-Total-Count: 47\nX-Total-Pages: 10\nX-Per-Page: 5\n```\n<\/code><\/pre>\n<p>This works well for relatively stable data sets where users need total counts and page navigation.<\/p>\n<p>For real-time endpoints, like the events API, Gusto uses <a href=\"https:\/\/docs.gusto.com\/embedded-payroll\/docs\/api-fundamentals#cursor-based-pagination\">cursor-based pagination<\/a> using <code class=\"\" data-line=\"\">starting_after_uuid<\/code> and <code class=\"\" data-line=\"\">limit<\/code>:<\/p>\n<pre class=\" language-bash\"><code class=\"\" data-line=\"\">\n```bash\nGET https:\/\/api.gusto.com\/v1\/events?starting_after_uuid=10ac74e7-d6f0-46c0-9697-8ec77ab475ba&amp;limit=5\n```\n<\/code><\/pre>\n<p>The cursor is simply the UUID of the last record. The response includes a header indicating whether more data exists:<\/p>\n<pre><code class=\"\" data-line=\"\">\nX-Has-Next-Page: true\n<\/code><\/pre>\n<p>When <code class=\"\" data-line=\"\">X-Has-Next-Page<\/code> is <code class=\"\" data-line=\"\">true<\/code>, the client extracts the UUID from the last record in the response and uses it as the <code class=\"\" data-line=\"\">starting_after_uuid<\/code> for the next request. Here\u2019s a sample code:<\/p>\n<pre class=\" language-python\"><code class=\"\" data-line=\"\">\n```python\nimport httpx\nimport asyncio\n\nasync def fetch_all_events(api_token: str):\n    all_events = []\n    cursor = None\n    has_more = True\n    base_url = &quot;https:\/\/api.gusto.com\/v1\/events&quot;\n\n    headers = {\n        &quot;Authorization&quot;: &quot;Bearer {api_token}&quot;,\n        &quot;X-Gusto-API-Version&quot;: &quot;2024-03-01&quot;\n    }\n\n    async with httpx.AsyncClient() as client:\n        while has_more:\n            params = {&quot;limit&quot;: 10}\n            if cursor:\n                params[&quot;starting_after_uuid&quot;] = cursor\n\n            response = await client.get(base_url, headers=headers, params=params)\n            response.raise_for_status()  # The caller must handle exceptions\n\n            events = response.json()\n            all_events.extend(events)\n\n            has_more = response.headers.get(&quot;X-Has-Next-Page&quot;) == &quot;true&quot;\n            if has_more and events:\n                cursor = events[-1][&quot;uuid&quot;]\n\n    return all_events\n```\n<\/code><\/pre>\n<p>The payroll API\u2019s cursor-based pagination keeps performance fast, regardless of the data set size. Using UUID-based cursors and indexed lookups, the database quickly finds the cursor record and retrieves the next batch, without counting or skipping rows.<\/p>\n<p>It also prevents data inconsistencies. As new events are added from payroll runs or employee updates, clients don\u2019t see duplicates or miss records. New entries appear at the beginning of the stream but don\u2019t affect the cursor\u2019s position.<\/p>\n<h3 id=\"explore-how-developers-can-benefit\">Explore How Developers Can Benefit<\/h3>\n<p>The Gusto Embedded design makes integration simple. The <code class=\"\" data-line=\"\">starting_after_uuid<\/code> parameter is intuitive: you just pass the last record\u2019s UUID. No complex cursor encoding or decoding is required, and the <code class=\"\" data-line=\"\">X-Has-Next-Page<\/code> header clearly signals when to stop, avoiding extra requests.<\/p>\n<p>This approach also scales effortlessly. Whether a company has 10 employees or 10,000, or generates 50 events or 5,000 per day, pagination performance is predictable. Gusto Embedded uses offset pagination when data is stable and cursors when data changes frequently\u2014showing how production APIs can stay both developer-friendly and performant.<\/p>\n<h2 id=\"choose-the-right-approach\">Choose the Right Approach<\/h2>\n<p><strong>Offset pagination<\/strong> is best for small data sets, prototypes, internal tools, or admin dashboards where speed of development matters. It works well when data rarely changes and users need traditional page numbers or bookmarking.<\/p>\n<p><strong>Cursor-based pagination<\/strong> is great for production applications, especially software-as-a-service (SaaS) platforms handling financial data, payroll, or real-time streams. It\u2019s ideal when data sets grow continuously, data consistency is critical (users can\u2019t miss or duplicate records), or users navigate sequentially, like in mobile apps with infinite scroll, activity feeds, or notifications.<\/p>\n<p><img decoding=\"async\" data-src=\"https:\/\/i.postimg.cc\/dV3sX4By\/pagination-offset-and-cursor-based.png\" alt=\"Offset vs. cursor-based pagination, image created by Manish Hatwalne\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" class=\"lazyload\" \/><\/p>\n<p>Before you decide which approach is right for your use case, ask yourself these three questions:<\/p>\n<ul>\n<li><strong>Can your pagination handle a ten-times growth without a rewrite?<\/strong> Offset often struggles with scale.<\/li>\n<li><strong>Do you need strict data integrity, where missing or duplicated records can cause financial errors or shake user confidence?<\/strong> If so, cursor pagination has you covered.<\/li>\n<li><strong>Is your data set small and relatively static?<\/strong> In that case, offset pagination suffices.<\/li>\n<\/ul>\n<blockquote><p>As a rule of thumb, offset works fine for small, rarely changing data sets, but when the stakes and data volume are high, cursor is the better choice.<\/p><\/blockquote>\n<h3 id=\"follow-best-practices-for-pagination-implementation\">Follow Best Practices for Pagination Implementation<\/h3>\n<ul>\n<li><strong>Tune your page size:<\/strong> Too small means more requests; too large slows responses. Find a number based on your own data that keeps performance and UX smooth.<\/li>\n<li><strong>Plan for compatibility:<\/strong> When migrating from offset to cursor pagination, support both temporarily or use API versioning with clear deprecation timelines.<\/li>\n<li><strong>Choose cursors wisely:<\/strong> Pick indexed, immutable, unique fields (like timestamp + ID combo) or UUIDs, if they\u2019re your primary identifiers.<\/li>\n<li><strong>Handle errors gracefully:<\/strong> If a cursor becomes invalid, return clear errors (<code class=\"\" data-line=\"\">400<\/code> Bad Request or <code class=\"\" data-line=\"\">410<\/code> Gone) and prompt users to restart pagination.<\/li>\n<li><strong>Document thoroughly:<\/strong> Explain cursor expiration and end-of-dataset behavior, and include working code examples.<\/li>\n<\/ul>\n<p>These best practices help your API stay fast, reliable, and developer-friendly, even as data and users grow.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Offset pagination works well for small data sets or quick prototypes. It\u2019s straightforward to implement and easy to understand. However, as your data grows, you\u2019ll run into performance issues and consistency problems with shifting records.<\/p>\n<p>Cursor-based pagination handles scale better. It uses unique position markers instead of offsets, which means no skipped or duplicate records when data changes, and query performance stays consistent even with millions of rows.<\/p>\n<p>If you\u2019re building APIs that need to handle growth without sacrificing data consistency, cursor pagination is worth the extra effort. The <a href=\"%5Bhttps:\/\/docs.gusto.com\/embedded-payroll\/docs\/introduction%5D(https:\/\/docs.gusto.com\/embedded-payroll\/docs\/introduction)\">Gusto approach<\/a> shows how this works in practice\u2014using the right pagination strategy for each endpoint based on how the data behaves.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A Developer\u2019s Guide to API Pagination: Offset vs. Cursor-Based You\u2019ve just built a shiny new feature that lists user transactions&#8230;.<\/p>\n","protected":false},"author":34,"featured_media":1007,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[4],"tags":[],"class_list":["post-1003","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-perspective"],"acf":{"exclude_from_embedded_resources":true,"popularity":0,"essentiality":0},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>A Developer&#039;s Guide to API Pagination: Offset vs. Cursor-Based - Embedded Blog<\/title>\n<meta name=\"description\" content=\"Learn how to efficiently fetch large data sets with API pagination. This guide compares offset and cursor-based methods, showing how the Gusto API implements cursor-based pagination for better performance at scale.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/embedded.gusto.com\/blog\/api-pagination\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A Developer&#039;s Guide to API Pagination: Offset vs. Cursor-Based - Embedded Blog\" \/>\n<meta property=\"og:description\" content=\"Learn how to efficiently fetch large data sets with API pagination. This guide compares offset and cursor-based methods, showing how the Gusto API implements cursor-based pagination for better performance at scale.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/embedded.gusto.com\/blog\/api-pagination\/\" \/>\n<meta property=\"og:site_name\" content=\"Embedded Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-11-21T21:59:25+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1920\" \/>\n\t<meta property=\"og:image:height\" content=\"1080\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Manish Hatwalne\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Manish Hatwalne\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A Developer's Guide to API Pagination: Offset vs. Cursor-Based - Embedded Blog","description":"Learn how to efficiently fetch large data sets with API pagination. This guide compares offset and cursor-based methods, showing how the Gusto API implements cursor-based pagination for better performance at scale.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/","og_locale":"en_US","og_type":"article","og_title":"A Developer's Guide to API Pagination: Offset vs. Cursor-Based - Embedded Blog","og_description":"Learn how to efficiently fetch large data sets with API pagination. This guide compares offset and cursor-based methods, showing how the Gusto API implements cursor-based pagination for better performance at scale.","og_url":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/","og_site_name":"Embedded Blog","article_published_time":"2025-11-21T21:59:25+00:00","og_image":[{"width":1920,"height":1080,"url":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png","type":"image\/png"}],"author":"Manish Hatwalne","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Manish Hatwalne","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#article","isPartOf":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/"},"author":{"name":"Manish Hatwalne","@id":"https:\/\/embedded.gusto.com\/blog\/#\/schema\/person\/6a45404f317af5cb806e92ce6f678629"},"headline":"A Developer&#8217;s Guide to API Pagination: Offset vs. Cursor-Based","datePublished":"2025-11-21T21:59:25+00:00","mainEntityOfPage":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/"},"wordCount":2108,"image":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#primaryimage"},"thumbnailUrl":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png","articleSection":["Developer Perspective"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/","url":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/","name":"A Developer's Guide to API Pagination: Offset vs. Cursor-Based - Embedded Blog","isPartOf":{"@id":"https:\/\/embedded.gusto.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#primaryimage"},"image":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#primaryimage"},"thumbnailUrl":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png","datePublished":"2025-11-21T21:59:25+00:00","author":{"@id":"https:\/\/embedded.gusto.com\/blog\/#\/schema\/person\/6a45404f317af5cb806e92ce6f678629"},"description":"Learn how to efficiently fetch large data sets with API pagination. This guide compares offset and cursor-based methods, showing how the Gusto API implements cursor-based pagination for better performance at scale.","breadcrumb":{"@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/embedded.gusto.com\/blog\/api-pagination\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#primaryimage","url":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png","contentUrl":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination.png","width":1920,"height":1080,"caption":"Developer's Guide to API Pagination"},{"@type":"BreadcrumbList","@id":"https:\/\/embedded.gusto.com\/blog\/api-pagination\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/embedded.gusto.com\/blog\/"},{"@type":"ListItem","position":2,"name":"A Developer&#8217;s Guide to API Pagination: Offset vs. Cursor-Based"}]},{"@type":"WebSite","@id":"https:\/\/embedded.gusto.com\/blog\/#website","url":"https:\/\/embedded.gusto.com\/blog\/","name":"Embedded Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/embedded.gusto.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/embedded.gusto.com\/blog\/#\/schema\/person\/6a45404f317af5cb806e92ce6f678629","name":"Manish Hatwalne","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/manish-draftdev-150x150.webp","url":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/manish-draftdev-150x150.webp","contentUrl":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/manish-draftdev-150x150.webp","caption":"Manish Hatwalne"},"description":"With 25+ years in tech, Manish has worked as a developer, architect, and consultant, mostly with distributed teams. He has taken multiple products from 0 to 1 at global startups. These days he builds AI\/ML systems, and in his spare time he enjoys writing, sometimes technical and sometimes just for solace.","url":"https:\/\/embedded.gusto.com\/blog\/author\/draftdev-mhatwalne\/"}]}},"images":{"large":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/Developers-Guide-to-API-Pagination-1120x630.png"},"authorDetails":{"id":34,"name":"Manish Hatwalne","avatar":"https:\/\/embeddedblog.wpengine.com\/wp-content\/uploads\/2025\/11\/manish-draftdev-150x150.webp"},"_links":{"self":[{"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/posts\/1003","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/users\/34"}],"replies":[{"embeddable":true,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/comments?post=1003"}],"version-history":[{"count":0,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/posts\/1003\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/media\/1007"}],"wp:attachment":[{"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/media?parent=1003"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/categories?post=1003"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/embedded.gusto.com\/blog\/wp-json\/wp\/v2\/tags?post=1003"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}