Diagnosing Ecto Pagination Inconsistencies: Limit/Offset vs Repo.page
Learn how to diagnose and fix common Ecto pagination issues caused by misuse of limit/offset, version differences, and changeset validation.
28 May 2026, 00:49 UTC

Recognizable Condition
When paginating Ecto queries you observe missing rows, duplicate entries, or query times that grow linearly with the page number.
Cause/Diagnostic Table
| Possible Cause | Diagnostic Indicator |
|---|---|
Manual limit/2 and offset/2 misuse (e.g., offset calculated incorrectly) | Results shift or skip rows when moving between pages |
Using Repo.page/3 on Ecto versions prior to 3.7 | Function undefined or returns unexpected struct |
| Changeset validation rejecting pagination parameters | Changeset returns errors for page or per_page fields |
| Large offset causing performance degradation | Query execution time increases noticeably for higher page numbers |
Ordered Checks
- Verify the Ecto version in
mix.exs; note whether it is>= 3.7. - Inspect the pagination code: are you using
Repo.page/3or manually buildinglimit/2andoffset/2? - If manual, confirm the offset calculation:
offset = (page - 1) * per_pageand that both values are non‑negative integers. - Run the query in
iex -S mixand capture the generated SQL withEcto.Adapters.SQL.query/3to see the actualLIMITandOFFSETclauses. - Check the changeset used for any form or API input that supplies
pageorper_page; ensure it permits those fields and casts them to integers. - Execute the same query with both approaches on a small fixture set and compare the returned rows.
Fixes Tied to Findings
- If the version is
< 3.7, replace calls toRepo.page/3with the explicitlimit/2andoffset/2formulation. - Correct the offset formula: ensure
offset = (page - 1) * per_pageand guard against negative values. - For Ecto
>= 3.7, preferRepo.page/3as it offsets the calculation internally and avoids off‑by‑one errors. - Add a changeset validation like
cast(params, [:page, :per_page]) |> validate_number(:page, greater_than: 0) |> validate_number(:per_page, greater_than: 0, less_than: 101)to prevent invalid pagination inputs. - When offset values become large (e.g., > 10000), consider keyset pagination using a unique ordered column instead of offset.
Escalation Criteria
- If after applying the fixes the query still returns incorrect rows, examine the underlying table for duplicate primary keys or missing indexes.
- If performance does not improve with keyset pagination, consult the database’s explain plan to verify that the chosen ordering column is indexed.
- Persistent errors in changeset validation indicate a mismatch between the schema field names and the parameters sent from the client; review the API contract.
Example: Switching from Manual Pagination to Repo.page
# Manual version (Ecto 2.x or 3.x < 3.7)
page = 2
per_page = 20
offset = (page - 1) * per_page
query =
from(u in User,
where: u.is_active == true,
limit: ^per_page,
offset: ^offset)
# Using Repo.page (Ecto 3.7+)
query = from(u in User, where: u.is_active == true)
paginated = Repo.page(query, page: 2, per_page: 20)
To verify the generated SQL, run in an IEx session:
Ecto.Adapters.SQL.query(Repo, Ecto.Adapters.SQL.to_sql(query), [])The returned
:rowswill contain the SQL string; check that it includesLIMIT 20and the correctOFFSETvalue.Practical way to check the result: inspect the struct returned by
Repo.page/3or the list fromRepo.all/2and assert thatlength(results) == per_page(except possibly the last page) and that thetotal_countfield matches the number of rows in the fixture.Limitations
- Large offset values can cause the database to scan and discard many rows, degrading performance.
- Integer‑typed offsets may overflow on extremely large tables; monitor the offset size.
- Keyset pagination requires a sortable, unique column (often the primary key) and may not be applicable when the ordering is complex or non‑unique.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.