Offset vs Keyset Pagination on Heroku Postgres: Which Scales Past 10⁶ Rows?
0 reputation · 21 Apr 2022, 07:50 UTC
0 reputation · 21 Apr 2022, 07:50 UTC
We need a pagination strategy for a Heroku‑hosted PostgreSQL table that will grow beyond one million rows and must serve API responses under 200 ms. The two documented approaches are offset pagination (LIMIT/OFFSET) and keyset pagination (WHERE id > last_id using a server‑side cursor).
Constraints include Heroku’s default 20‑connection pool, pgBouncer running in transaction‑pooling mode which can close long‑lived cursors, and the requirement for a stable, monotonic ordering column to avoid gaps or duplicates. Counting total rows for offset pagination is expensive, while keyset pagination eliminates the count but may hold cursors longer than the pool permits.
Given these trade‑offs, which method provides predictable latency and connection‑pool safety at scale?
35775 reputation · 21 Apr 2022, 18:56 UTC
For Heroku Postgres tables exceeding 1 million rows with a 200ms latency target, keyset pagination using a stateless WHERE clause (e.g., WHERE id > last_seen_id ORDER BY id LIMIT 50) is the only approach that scales predictably within connection limits.
OFFSET 100,000 queries expensive regardless of indexes. On Heroku Postgres, this typically exceeds 200ms.WHERE id > last_seen_id ORDER BY id LIMIT 50 uses an index seek, executing in O(log n) time regardless of table size.id (serial/bigserial) or a created_at timestamp with a unique tiebreaker.SELECT * FROM items WHERE id > $1 ORDER BY id LIMIT 50; Pass the last id from the current page as the parameter.EXPLAIN ANALYZE to verify index usage and measure actual execution time.pg_stat_activity showing idle in transaction sessions approaching the 20-connection limit.pgBouncer logs reporting pool error: all connections are in use.-- Test keyset performance on your 1M+ row table
EXPLAIN ANALYZE SELECT * FROM items WHERE id > 1000000 ORDER BY id LIMIT 50;
-- Check current connection state
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
id column is indexed (primary key or explicit index).SELECT COUNT(*) FROM items cached in Redis or calculated via reltuples in pg_class.Action: Adopt keyset pagination with stateless WHERE clauses. Offset pagination should be limited to recent pages (offset < 10,000) or replaced entirely for deep navigation.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.