Hot Take: Offset Pagination Is Fine for Small Datasets – Stop Using Cursor for Next.js 15
If you’ve spent any time in backend or frontend circles lately, you’ve probably heard the mantra: “Cursor pagination is better than offset pagination, always use cursor.” But let’s cut through the dogma: for small datasets in Next.js 15 apps, offset pagination is perfectly fine, and forcing cursor pagination is often unnecessary overkill.
First: A Quick Refresher on Pagination Types
Offset pagination is the classic approach: you specify a page number and a limit of items per page, then calculate an offset (number of items to skip) as (page - 1) * limit. It’s simple, intuitive, and lets users jump to arbitrary pages (e.g., page 5 of 10) easily.
Cursor pagination, by contrast, uses a unique, sequential pointer (usually a timestamp plus a unique ID) to mark the “end” of the current page, then fetches the next set of items after that cursor. It avoids issues like missing or duplicate records when data is inserted/deleted mid-pagination, and performs better on very deep pages. But it’s more complex to implement, doesn’t support arbitrary page jumps, and requires careful indexing on your database.
Why Cursor Pagination Is Overkill for Small Datasets
The core downsides of offset pagination only matter at scale:
- Performance on deep pages: Databases have to scan and skip thousands of rows for offset 10000, which gets slow. But for small datasets (think <10,000 total records, or even <100,000 if traffic is low), this is never an issue.
- Data consistency: If a new record is inserted at the top of the list while a user is paginating, offset pagination might skip that record or show a duplicate. But for low-write datasets (personal blogs, internal admin panels, small e-commerce stores), this edge case almost never occurs, and even if it does, the impact is negligible.
For context: a small blog with 500 posts, paginated at 10 per page, only has 50 total pages. Even if you have 5,000 products in a small store, that’s 500 pages at 10 per page. Offset pagination handles this effortlessly in Next.js 15, with no performance hits.
Implementing Offset Pagination in Next.js 15 (It’s Easy!)
Next.js 15’s App Router and Server Components make offset pagination trivial to implement. You can read page parameters directly from searchParams, fetch data in your server component, and render pagination controls. Here’s a simple example using Prisma as an ORM:
// app/posts/page.tsx
import { prisma } from '@/lib/prisma';
interface PostsPageProps {
searchParams: { page?: string };
}
async function getPosts(page: number, limit = 10) {
const offset = (page - 1) * limit;
const posts = await prisma.post.findMany({
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' },
});
const totalPosts = await prisma.post.count();
const totalPages = Math.ceil(totalPosts / limit);
return { posts, totalPages, currentPage: page };
}
export default async function PostsPage({ searchParams }: PostsPageProps) {
const currentPage = Number(searchParams.page) || 1;
const { posts, totalPages, currentPage: page } = await getPosts(currentPage);
return (
All Posts
{posts.map((post) => (
{post.title}
))}
{page > 1 && (
Previous
)}
Page {page} of {totalPages}
{page < totalPages && (
Next
)}
);
}
No complex cursor logic, no extra database indexes, no fighting with opaque cursor strings. Just simple, readable code that works immediately.
When Should You Use Cursor Pagination?
Cursor pagination is still the right choice for large-scale, high-traffic datasets: social media feeds, product listings with millions of items, or apps with frequent writes where data consistency during pagination is critical. But for the vast majority of small Next.js 15 apps, you don’t need it.
The Bottom Line
Stop overcomplicating your stack. Use offset pagination for small datasets in Next.js 15, and save cursor pagination for when you actually hit the scale where offset’s limitations matter. The best solution is almost always the simplest one that works for your use case.
Top comments (0)