MongoDB Pagination with skip() and limit()

Learn to paginate MongoDB results using skip() and limit(). This tutorial covers syntax, efficient paging for large datasets, and common pitfalls.

Focus: MongoDB pagination with skip and limit

Sponsored

You've built an app that fetches 10,000 documents from MongoDB and renders them all at once. The page grinds to a halt, the browser freezes, and your users bounce. The classic fix is pagination — but MongoDB doesn't have a built-in .page() method. Instead, you combine skip() and limit() to fetch a specific slice of data. In this lesson, you'll learn how to build pagination with skip and limit, why it's powerful, and where it falls short for enormous collections.

The problem this lesson solves

When your MongoDB collection grows — say, a users collection with 50,000 records — sending every single document to the client is a disaster. It wastes network bandwidth, slows down your API, and makes the frontend glaze over. Without a paging strategy, your app's performance degrades linearly with collection size.

Pagination solves this by breaking results into manageable pages. Users see 20, 50, or 100 items at a time and can click "Next" or scroll infinitely. MongoDB gives you two cursor methods to implement this: limit() (how many documents to return) and skip() (how many to ignore from the start). Together, they form the backbone of MongoDB pagination with skip and limit.

Pro tip: If your collection is huge (millions of records), skip-based pagination becomes slow because MongoDB must scan and discard skipped documents. But for most apps, it's perfectly fine — we'll cover alternatives later.

Core concept / mental model

Think of a cursor as a queue of documents matching your query. The database processes the queue in natural order (or the order specified by .sort()).

  • limit(n) — "Give me at most n documents from this queue."
  • skip(m) — "Skip the first m documents, then give me the rest."

Together, they define a window: skip the first m, then take the next n.

For page number page (starting at 1) with pageSize items per page:

skip = (page - 1) * pageSize
limit = pageSize

So page 2 of 100 items (pageSize=20) means skip(20).limit(20) — you jump over the first 20 and grab the next 20.

In MongoDB syntax, chaining looks like:

cursor = collection.find(query).sort("created_at", -1).skip(skip).limit(limit)

The order of .skip() and .limit() in the chain doesn't affect the result — MongoDB applies both server-side.

How it works step by step

Step 1: Build your query. Start with a filter that selects the documents you want, e.g., {"status": "active"}.

Step 2: Sort deterministically. Pagination requires a stable order. Without a sort, MongoDB returns documents in natural order (often insertion order), which can change. Always sort by a unique or near-unique field like _id or created_at.

Step 3: Calculate skip and limit. Use the formula above with the requested page number.

Step 4: Chain methods on the cursor. Call .sort(), then .skip(), then .limit().

Step 5: Execute and convert to list. In Python, you'll typically do list(cursor) to fetch the documents.

Let's see it in action.

Hands-on walkthrough

Assume you have a Python app using PyMongo and a products collection with thousands of entries. You want to paginate with 10 items per page.

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017")
db = client["shop"]
products = db["products"]

def get_page(page_number, page_size=10):
    skip = (page_number - 1) * page_size
    cursor = products.find({}) \
                     .sort("_id", 1) \
                     .skip(skip) \
                     .limit(page_size)
    return list(cursor)

# Example: fetch page 3
page_3 = get_page(3)
print(f"Got {len(page_3)} products")
for p in page_3[:2]:
    print(p["name"])

Expected output:

Got 10 products
Product 21
Product 22

Now, let's add a total count so the client can render page numbers.

from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route("/api/products")
def products_page():
    try:
        page = int(request.args.get("page", 1))
        page_size = int(request.args.get("page_size", 10))
    except ValueError:
        return jsonify({"error": "page and page_size must be integers"}), 400

    skip = (page - 1) * page_size
    query = {}
    total = products.count_documents(query)  # for pagination metadata
    items = list(
        products.find(query)
                .sort("_id", 1)
                .skip(skip)
                .limit(page_size)
    )
    return jsonify({
        "page": page,
        "page_size": page_size,
        "total": total,
        "total_pages": (total + page_size - 1) // page_size,
        "items": items
    })

if __name__ == "__main__":
    app.run(debug=True)

This API endpoint returns a clean JSON payload that any frontend can consume. Note how we calculate total_pages with integer arithmetic.

Compare options / when to choose what

Approach How it works Pros Cons Best for
skip() + limit() Skip first (page-1)*size, take size Simple, works with any query, easy to implement O(n) on large offsets; slow deep pages Small/medium datasets (< 1M docs), admin panels
_id-based keyset Filter on _id > last_seen_id, take limit Fast, stable, uses index Requires ordered traversal, no page numbers Large datasets, infinite scroll, social feeds
$facet aggregation Compute total and page in one stage Single round-trip, can combine with aggregation Verbose, may be overkill for simple cases Complex analytics, dashboards

For most tutorials and typical web apps, skip/limit is the go-to. You'll switch to keyset pagination when you hit performance issues on deep pages.

Troubleshooting & edge cases

1. Missing sort() causes inconsistent pages. If you don't sort, documents may shift between queries. Always sort by a unique field. Use _id if nothing else.

2. skip larger than the collection count. If your skip exceeds total documents, MongoDB returns an empty list — not an error. Handle it gracefully in your API (e.g., return 404 or an empty page).

3. Negative or non-integer page or page_size. Validate input to prevent Python errors or weird behavior.

4. Very deep pages are slow. Because MongoDB has to scan and discard skipped documents. If you see performance degradation, switch to keyset pagination.

5. Duplicate results when new documents arrive. If a document is inserted between page requests, you might see it appear on two pages. Sort by _id (which is monotonically increasing) minimizes this.

6. limit(0) means no limit. Don't accidentally pass 0. Use a sensible default like 10 or 20.

What you learned & what's next

You now explain the core idea behind MongoDB pagination with skip and limit — slicing results into pages with skip() and limit() — and you've completed a practical exercise building a Flask API that serves paginated product data. You also know when to choose this approach versus alternatives like keyset pagination.

Next up in the MongoDB track, you'll deepen your skills — likely exploring indexes to speed up these queries, or learning aggregation pipelines for advanced analysis. Keep practicing!

Practice recap

Try building a paginated API for a 'posts' collection with 50 documents. Add endpoints /api/posts?page=1&page_size=5 and verify the output. Then, test with page=1000 and see what happens — handle it gracefully. Finally, compare performance with and without an index on the sort field.

Common mistakes

  • Forgetting to sort() the query — results may be inconsistent across pages.
  • Using skip() with huge numbers (e.g., 100,000) — performance tanks; consider keyset pagination.
  • Not validating page/page_size inputs — negative values cause errors or odd behavior.
  • Passing limit(0) — this returns all documents, not an empty page.

Variations

  1. Keyset pagination using _id: filter on {_id: {$gt: lastSeenId}} and limit.
  2. Aggregation with $facet to compute total count and page in one go.
  3. Indexed filter-based pagination with a timestamp field instead of _id.

Real-world use cases

  • An e-commerce product listing API that displays 20 items per page.
  • A blog's admin panel where you page through 5000 post documents.
  • A mobile chat app fetching older messages in batches via infinite scroll.

Key takeaways

  • skip() and limit() combine to form something like LIMIT/OFFSET in SQL.
  • The formula is skip = (page-1)*pageSize; limit = pageSize.
  • Always sort by a stable field for consistent pagination.
  • Deep pages are slow with skip; consider keyset pagination for large datasets.
  • Return total count and total pages in your API response for UI completeness.
  • Validate page and page_size inputs to avoid runtime errors.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.