<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna" -->

---
title: OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset...
description: A real-world case study showing how OFFSET-based pagination caused a 47-second query on an admin orders table, and how switching to keyset (seek) pagination...
canonical: https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset Pagination Took 12ms | daily.dev
og:description: A real-world case study showing how OFFSET-based pagination caused a 47-second query on an admin orders table, and how switching to keyset (seek) pagination...
og:url: https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna
og:image: https://api.daily.dev/og/posts/H7BkJKxna.png
og:image:alt: OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset Pagination Took 12ms
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset Pagination Took 12ms

**[Medium](https://daily.dev/sources/medium_js)** · 8 min read · 9 upvotes · 2 comments

## Summary

A real-world case study showing how OFFSET-based pagination caused a 47-second query on an admin orders table, and how switching to keyset (seek) pagination reduced it to 12ms. Explains why OFFSET cost scales with skip depth rather than result size, how to implement keyset pagination with a composite index on (created_at DESC, id DESC), and a practical migration path: cap legacy OFFSET at 5000, add composite indexes first, convert hot admin views first, and replace deep-paging scripts with cursor-based exports. Also covers pitfalls like COUNT(*) for totals and ORM-generated OFFSET defaults.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://ai.plainenglish.io/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-e5f068d204c2>

## Questions this post answers

### Why does Postgres OFFSET pagination get slow on deep pages?

OFFSET forces Postgres to walk and discard every skipped row before returning the requested page, so cost scales with how far you skip rather than how many rows you return. A page-401 query with OFFSET 10000 took 47 seconds under load versus 80ms for page 1, because the planner still touches tens of thousands of tuples before discarding them.

_Developers debugging sudden admin-panel slowdowns can find similar deep-dives on daily.dev to fix the query shape, not just the symptom._

### How do I implement keyset (seek) pagination in PostgreSQL instead of OFFSET?

Use a WHERE clause that filters on the last row's cursor values instead of skipping rows: WHERE created_at < $cursor_created_at OR (created_at = $cursor_created_at AND id < $cursor_id), ordered by created_at DESC, id DESC, with LIMIT 25. Pair it with a composite index on (created_at DESC, id DESC) so the planner does an index scan instead of a multi-second discard loop.

_Teams choosing a pagination strategy for growing tables can track patterns like this one on daily.dev before it becomes a production incident._

### What percentage of admin panel traffic actually needs deep pagination past page 5?

Logging OFFSET depth for two weeks before a pagination cutover showed 92% of traffic never went past page 5. The remaining pathological traffic came from a handful of power users and two internal scripts scraping history, who were moved to a cursor-based export job instead of paging through the UI.

_Engineers deciding whether to invest in cursor-based exports versus deep pagination can compare real traffic data like this via daily.dev._

## Community discussion

Top comments from developers on daily.dev.

**@pdfopsdev** · 1 upvotes

> ran into this exact wall with an admin doc-listing view — worth flagging keyset pagination breaks down fast if your sort column isn't unique. ties on created_at need a tiebreaker id in the composite key or rows silently skip/duplicate across pages.

## Similar posts on daily.dev

- [Pagination in Entity Framework Core: Why Skip/Take Falls Apart on Hot Tables - Chris Woody Woodruff](https://daily.dev/posts/pagination-in-entity-framework-core-why-skip-take-falls-apart-on-hot-tables---chris-woody-woodruff-la973vnde) · We Are .NET · 1 upvotes · 0 comments
- [Stop Crashing Your Servers: The Ultimate Guide to Pagination \(Offset vs. Cursor\)](https://daily.dev/posts/stop-crashing-your-servers-the-ultimate-guide-to-pagination-offset-vs-cursor--pzqbdxfml) · Medium · 0 upvotes · 0 comments
- [Offset and Keyset Pagination with Spring Data JPA](https://daily.dev/posts/offset-and-keyset-pagination-with-spring-data-jpa-nya5hmupa) · Jakarta EE · 0 upvotes · 0 comments

---

Tags: [#sql](https://daily.dev/tags/sql), [#postgresql](https://daily.dev/tags/postgresql)

[View this post on daily.dev](https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset Pagination Took 12ms","url":"https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna"},"datePublished":"2026-07-15T19:32:23.418Z","dateModified":"2026-09-13T21:29:20.244Z","description":"A real-world case study showing how OFFSET-based pagination caused a 47-second query on an admin orders table, and how switching to keyset (seek) pagination...","image":"https://media.daily.dev/image/upload/s--58gMhC4P--/f_auto/v1722860399/public/Placeholder%2012","thumbnailUrl":"https://media.daily.dev/image/upload/s--58gMhC4P--/f_auto/v1722860399/public/Placeholder%2012","isAccessibleForFree":true,"articleSection":"Medium","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Medium","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/medium","url":"https://daily.dev/sources/medium_js"},"commentCount":2,"discussionUrl":"https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":9},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":2}],"keywords":"sql,postgresql","timeRequired":"PT8M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Medium","item":"https://daily.dev/sources/medium_js"},{"@type":"ListItem","position":3,"name":"OFFSET 4000 Made Our Admin Panel Take 47 Seconds. Keyset Pagination Took 12ms"}]}
{"@context":"https://schema.org","@type":"WebPage","@id":"https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna","comment":[{"@type":"Comment","text":"ran into this exact wall with an admin doc-listing view — worth flagging keyset pagination breaks down fast if your sort column isn’t unique. ties on created_at need a tiebreaker id in the composite key or rows silently skip/duplicate across pages.","datePublished":"2026-07-16T18:04:06.136Z","url":"https://daily.dev/posts/H7BkJKxna#c-0XbcV3m9w","author":{"@type":"Person","name":"PDFops","url":"https://daily.dev/pdfopsdev","image":"https://media.daily.dev/image/upload/s---8isRBKc--/f_auto/v1782922291/avatars/avatar_orjMeK8QKaaVZwGq7ScPz?_a=BAMAMicg0"},"interactionStatistic":{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":1}}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/offset-4000-made-our-admin-panel-take-47-seconds-keyset-pagination-took-12ms-h7bkjkxna#faq","mainEntity":[{"@type":"Question","name":"Why does Postgres OFFSET pagination get slow on deep pages?","acceptedAnswer":{"@type":"Answer","text":"OFFSET forces Postgres to walk and discard every skipped row before returning the requested page, so cost scales with how far you skip rather than how many rows you return. A page-401 query with OFFSET 10000 took 47 seconds under load versus 80ms for page 1, because the planner still touches tens of thousands of tuples before discarding them. Developers debugging sudden admin-panel slowdowns can find similar deep-dives on daily.dev to fix the query shape, not just the symptom."}},{"@type":"Question","name":"How do I implement keyset (seek) pagination in PostgreSQL instead of OFFSET?","acceptedAnswer":{"@type":"Answer","text":"Use a WHERE clause that filters on the last row's cursor values instead of skipping rows: WHERE created_at < $cursor_created_at OR (created_at = $cursor_created_at AND id < $cursor_id), ordered by created_at DESC, id DESC, with LIMIT 25. Pair it with a composite index on (created_at DESC, id DESC) so the planner does an index scan instead of a multi-second discard loop. Teams choosing a pagination strategy for growing tables can track patterns like this one on daily.dev before it becomes a production incident."}},{"@type":"Question","name":"What percentage of admin panel traffic actually needs deep pagination past page 5?","acceptedAnswer":{"@type":"Answer","text":"Logging OFFSET depth for two weeks before a pagination cutover showed 92% of traffic never went past page 5. The remaining pathological traffic came from a handful of power users and two internal scripts scraping history, who were moved to a cursor-based export job instead of paging through the UI. Engineers deciding whether to invest in cursor-based exports versus deep pagination can compare real traffic data like this via daily.dev."}}]}
```

