Paginating Active Record Queries in Rails
Replace unbounded Active Record queries with paginated scopes to limit memory use and database load in Rails applications.
05 Mar 2026, 23:10 UTC

The problem: loading every row into memory
A controller action like @posts = Post.all works with a few dozen rows, but in production it can load thousands of Active Record objects into the application heap on a single request. That increases memory use, slows page delivery, and can exhaust the web worker's available memory. Pagination limits how many rows the database returns per request, so the application only materializes the records needed for the current page.
How Rails slices a result set
Active Record translates limit and offset into SQL clauses. LIMIT caps the number of rows returned, and OFFSET tells the database how many rows to skip before returning results. Rails does not paginate automatically, so most applications add a gem such as Kaminari or will_paginate to calculate page numbers and render navigation links.
Worked example: adding Kaminari pagination
This example assumes a Rails application with a Post model and an index action. Run commands from the application root; bundle install requires write access to the Gemfile and Gemfile.lock.
1. Add the gem
# Gemfile
gem 'kaminari'
Run bundle install to install the gem and update the lockfile.
2. Paginate in the controller
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.order(created_at: :desc).page(params[:page]).per(20)
end
end
page(params[:page]) reads the page query parameter and calculates the offset. per(20) limits the result to 20 rows. When the parameter is absent, Kaminari defaults to page 1.
3. Render navigation links
<%= paginate @posts %>
Add this to app/views/posts/index.html.erb above or below the record table. Kaminari generates Previous, Next, and numbered links based on the current page and total count.
Limitations of offset-based pagination
Kaminari and will_paginate use offset-based pagination by default. Numbered pages are convenient, but there are trade-offs:
- Database scan cost: A large offset can force the database to scan and discard skipped rows before returning the slice. Actual cost depends on the adapter, indexes, and query shape.
- Shifting results: Inserts or deletes while a user navigates can cause the same record to appear on multiple pages, or to disappear.
For very large feeds or logs, some teams use cursor-based pagination instead, filtering with WHERE id > last_seen_id. That avoids scanning skipped rows but does not support jumping directly to an arbitrary page number.
How to verify the change
- Start the Rails server in development and request the index page.
- Inspect the application log. You should see a query containing
LIMITandOFFSET, though exact formatting depends on the Rails version, database adapter, and logger configuration. - Add
?page=2to the URL and confirm the offset changes and different records appear. - If the log still shows an unbounded query, confirm the controller uses the
pagescope and that the gem is installed.
Precise memory comparison requires a profiling tool such as rack-mini-profiler; observing fewer rows in the log is a practical sanity check, not a measurement.
Closing
Replace unbounded Model.all calls in index actions with a paginated scope when a collection can grow beyond a few hundred rows. Kaminari is a reasonable starting point for admin interfaces and search results. If users only move forward through millions of records, evaluate cursor-based pagination before committing to numbered pages.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.