- Donald Knuth
In this post, we're going to explore how to reduce memory used by your Rails applications by only querying the data you actually need from the database.
Early in a project, it's common (and practical) to fetch the whole records, including all the columns using Model.all
or Model.where
query methods. At this stage, it doesn't even matter. Most models don't have that many columns, and the data isn't large.
However, as the application grows, tables grow, new columns are added (some holding large blobs of data). When you fetch these records from the database into your Rails application, all this data is stored in application memory and the memory usage starts to creep up.
Let's look at a simple example. We'll continue where we left off in the previous post on profiling Rails applications with Debugbar. In that post, we created a Member
model backed by a members
table with just three columns: name
, email
, and team_id
.

Now imagine we get a new requirement: each member needs a bio
. It’s a reasonable change, so let's add a new column on the members
table.
bin/rails generate migration AddBioToMembers bio:text
bin/rails db:migrate
Next, we update the seeds file to generate some fake bios. Each bio will contain 5–10 paragraphs of text. With 500 rows, that’s about 2,500–5,000 paragraphs total—enough to start noticing memory impact.
# db/seeds.rb
require "faker"
10.times do
team = Team.create(name: Faker::Company.name)
end
Team.find_each do |team|
50.times do
Member.create(
name: Faker::Name.name,
email: Faker::Internet.email,
team_id: team.id,
join_date: Faker::Date.between(from: 1.year.ago, to: Date.today),
bio: Faker::Lorem.paragraphs(number: rand(5..10)).join("\n\n")
)
end
end
Now let’s load the page that lists all members.
# app/controllers/members_controller.rb
class MembersController < ApplicationController
def index
@members = Member.includes(:team).order(:name)
end
end
The view renders a simple table with just the name
, email
, and team name:
<% @members.each do |member| %>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<%= member.name %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<%= member.email %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<%= member.team.name %>
</td>
</tr>
<% end %>
Let's load the page. It works fine and shows all the expected data. For this, Rails runs two queries:
- one query to fetch all 500 members, instantiating 500
Member
objects - another query for teams for those members, instantiating 10
Team
objects.

So far, so good… but here's the problem: we’ve now got 500 full-text bios sitting in memory—even though we're not displaying them.
Let's inspect the memory using Rack MiniProfiler.
You can view memory usage by appending ?pp=profile-memory
to any URL while MiniProfiler is enabled. You can also fine-tune the output with a few query parameters:
memory_profiler_allow_files
– filter to only include certain filesmemory_profiler_ignore_files
– exclude specific filesmemory_profiler_top
– control how many lines to show (defaults to 50)
Since we're only interested in application's memory usage, let's reload the page with following URL:
http://localhost:3000/members?pp=profile memory&memory_profiler_allow_files=app

As you can see, we are allocating and retaining lots of memory, and we are not even showing the member bios on this page. The reason is in the query Rails runs behind the scenes:
SELECT * FROM members;
Rails is fetching all columns from the members
table, including the bio
. Even though we only need a member’s name
, email
, and team
, we’re pulling in everything.
And since each bio
is several paragraphs of text, this adds up quickly. All that data is loaded into memory for every row—even though it's never used.
So let’s fix that by selecting only the columns we actually need.
class MembersController < ApplicationController
def index
@members = Member
.includes(:team)
.select(:id, :name, :email, :team_id)
.order(:name)
end
end
Now reload the page.

As you can see, we are down from over ~600 KB to ~5KB. That's much, much better. By selecting only the necessary columns, we’ve reduced a lot of memory usage on this page.
But, premature optimization...?
Now, you might be thinking: “Come on, it’s just 600 KB—who cares?” Didn't Donald Knuth say "premature optimization is the root of all evil" or something like that?
And honestly, that’s fair… to a point. This example is intentionally simple to make the concept clear. But in real-world applications, especially those running in memory-constrained environments, handling thousands of concurrent users, or processing large datasets—these kinds of small inefficiencies add up fast.
It’s not just about this one page or this one query. It’s about building good habits. If you're consistently over-fetching data across your app, the memory overhead can become very real, very quickly. And if you're dealing with serialized blobs, file metadata, rich text, or large JSON fields? You’ll definitely feel the impact.
That said, this doesn’t mean you need to grep your codebase for every instance of Model.all
and start micro-optimizing everything. Good caching can mitigate a lot of this. But if you're working on pages that are heavily used, run complex queries, or load lots of associated records—it’s worth being intentional about what you're fetching.
That's a wrap. I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.