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
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 mostndocuments from this queue."skip(m)— "Skip the firstmdocuments, 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
- Keyset pagination using _id: filter on {_id: {$gt: lastSeenId}} and limit.
- Aggregation with $facet to compute total count and page in one go.
- 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.