
Performance problems in Rails applications are sneaky. Generally speaking, nobody opens tickets that say “my application is slower than it was last month (about 20%)”. What you do get instead are vague complaints from team members about a p95 latency that is climbing every week or a background job that used to take 2 seconds now taking 40 seconds to finish.
Nine times out of ten, the problem is going to be a query that used to be fast, and now it’s not. When that query was first written, it had 500 records in the table. Now, it’s got 500K records, and it’s running a full table scan on every page load. Each new row means slightly more scan time and latency. This increase in time continues until it reaches a point when it can no longer be ignored. And by then, it’s usually been around for a few weeks.
This article walks you through a repeatable debugging loop: identifying the bottleneck, understanding why it’s slow, fixing it, and verifying that the fix has actually worked.
It’s not a comprehensive guide to every ActiveRecord optimization (the linked articles at the bottom cover those in depth). It’s just the workflow you can reach for when something is clearly wrong and you need to locate the issue fast.
Finding Where the Time Goes
Before digging through logs or adding logger.debug statements everywhere, you should open AppSignal’s Performance > Charts and look at the Event Groups (Relative) chart. It gives you a quick breakdown of where request time is actually being spent, whether that is active_record, view, process_action, external HTTP calls, or something else entirely.
If active_record is taking up a large chunk of the total response time (around 55% in our case), there is a good chance the database is where the slowdown is coming from.

This easy check helps you avoid unnecessary guesswork. For instance, if your database is consuming the majority of your response time, there’s no need to optimize template rendering or get into your view partials.
From there, go to Performance > Slow Queries in AppSignal. AppSignal ranks each query based on both duration and frequency, and this ranking matters more than you may think. For example, a query taking 900 ms that executes only once a day will not be that noticeable to your users. A 150 ms query that executes on every request when someone looks at a product listing will take away a lot of response time at scale. The high-frequency offenders will show up first in the impact ranking created by AppSignal, and that’s almost always the best place to start.
Click any query, and a detail panel will open, showing the mean duration, throughput over the selected time window, and the actions that trigger it. Be sure to check that last piece to have the full context before you write your solution. A slow query firing only on an admin CSV export is a lower priority than one firing on your main product index.
Always make sure you understand the reach of the problem before you write any fix.

Understanding Why It's Slow
Clicking into a slow query reveals that specific request’s performance sample, including a full timeline of all events related to it. This is where patterns start to emerge, and these are the two that cause most production query issues within Rails:
- The first one is a single long
sql.active_recordbar: that is, one query that takes between 300 and 500 milliseconds on a table that should typically respond within milliseconds. If a table has 200,000 rows, then the database must sequentially scan each of them to find the ones that match yourWHEREclause. This is usually fine when the table is small, though. - The second pattern looks very different, as there are many short bars that are stacked up. If you’ve got 15 to 20 queries, each taking 15 ms, then you’re likely looking at an N+1 problem. This typically happens when you’ve performed a collection load without preloading associations, and ActiveRecord has executed a query one time per record while looping through the collection. Individually, those queries are not an issue, but if you perform 1,000 queries, this will create an N+1 problem, and AppSignal will highlight this with an N+1 warning banner in the sample view.


Each of these patterns will be differently displayed on the timeline, and checking it prior to reading any code will save you time. Once you have formed a hypothesis, copy the SQL from the Slow Queries panel and run EXPLAIN ANALYZE against your database directly:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42;
Seq Scan on orders (cost=0.00..4821.00 rows=1 width=256) (actual time=0.042..38.214 rows=1 loops=1) Filter: (user_id = 42) Rows Removed by Filter: 195432 Planning Time: 0.3 ms Execution Time: 38.4 ms
A Seq Scan with 195,000 rows removed by filter confirms a missing index. In this case, the database had to perform a full table scan in order to find the one matching row. When an index was created, running the same query resulted in an Index Scan replacing the Seq Scan and reducing the query execution time by an order of magnitude.
As for N+1, however, the EXPLAIN ANALYZE command on one of those queries doesn’t reveal the problem directly. The problem isn’t the shape of an individual query, it’s the total number of them. The event timeline already told you what you need. The EXPLAIN ANALYZE command is used primarily to confirm suspected missing indexes, as indicated by the event timeline.
The output can get quite verbose on complex queries. The guide to using/explaining EXPLAIN ANALYZE found in the PostgreSQL documentation is rather comprehensive. However, in this situation, simply identifying a Seq Scan compared to an Index Scan is often enough to validate what the event timeline has already suggested.
Fix It, Prove It
Once you know what the timeline is showing, it's time to fix it. The approach differs depending on the pattern — missing index, N+1, or an unscoped query loading more data than needed.
Missing Index
If EXPLAIN ANALYZE shows Seq Scan on a filtered column, add an index like:
# db/migrate/20260522_add_index_to_orders_user_id.rb class AddIndexToOrdersUserId < ActiveRecord::Migration[7.1] def change add_index :orders, :user_id end end
Run rails db:migrate, and then re-run EXPLAIN ANALYZE. Now, Seq Scan should flip to Index Scan, and if it doesn't, you need to run ANALYZE orders; first to refresh the planner’s statistics.
For queries that consistently filter on two columns together, a composite index beats two separate ones:
add_index :orders, [:user_id, :status]
There is a cost per write associated with indexes since every insert and update has to maintain the index for that record. As a result, a rule of thumb would be to add them only where the performance gains justify the additional maintenance cost.
N+1 Queries
# Before: one DB hit per order record orders = Order.where(user: current_user) orders.each { |o| puts o.customer.name } # After: two queries total, regardless of result set size orders = Order.where(user: current_user).includes(:customer) orders.each { |o| puts o.customer.name }
includes() will preload the associated records in a single query instead of making a separate query for each one. This reduces the query count from N+1 down to two. If you need explicit control, preload forces a separate query, and eager_load forces a JOIN. In a typical N+1 case, includes() picks the right strategy without any guidance.
Unscoped or Overloaded Queries
ActiveRecord loads all columns by default. On tables with large text columns, serialized JSON, or binary data, loading columns you never use only adds overhead in network transfer and memory allocation.
# Loads every column for every matching record User.where(active: true).map(&:email) # Load only the columns the code uses User.where(active: true).select(:id, :email)
This rarely matters with narrow tables, but when it comes to wide ones that include serialized or binary data, it shows up directly in AppSignal's allocation numbers.
Verifying the Fix
AppSignal also adds deploy markers to the performance graph, making it easier to see how response times change before and after a fix.

After deploying, here’s what you need to verify:
- The response time should drop significantly at that line.
- Pull a new performance sample and check if the long
sql.active_recordbar has been reduced or if the N+1 stack has collapsed down to one or two queries.
If the deployment hasn’t resolved the slow query, then there is likely another slow query behind the one you’ve just fixed. Go back to the Slow Queries and repeat this process. Once verified, set up an anomaly detection trigger based on that same action. Any regression from a schema change or unexpected data growth will surface as an alert instead of a user complaint.
Wrap-Up
The loop is the same every time when resolving a performance issue:
- Find the bottleneck using Event Groups and Slow Queries.
- Diagnose the cause from the event timeline and
EXPLAIN ANALYZE. - Fix the problem with an index migration,
includes(), or column scoping. - Verify the results of your changes using Deploy Markers and new Performance Sample collections in addition to the Slow Queries List.
Most issues affecting Rails performance stay invisible until a table grows large enough for latency to become noticeable. No one files a bug report saying a site feels slightly slower over time. The "damage" from a few extra milliseconds per request, for thousands of requests daily, adds up quickly.
This workflow will not catch every database issue, but it does help find the most common Rails performance problems quickly: things like missing indexes, N+1 queries, and slow ActiveRecord queries. In most cases, that is enough to go from “the app feels slow” to a working fix in less than an hour.
If you aren’t already using AppSignal on your Rails app, setup takes a few minutes. Even the Slow Queries page alone can reveal performance issues that may have been affecting your app for a long time without anyone noticing.
Also, read about:
Happy coding!
Wondering what you can do next?
Finished this article? Here are a few more things you can do:
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media
Most popular Ruby articles

What's New in Ruby on Rails 8
Let's explore everything that Rails 8 has to offer.
See more
Measuring the Impact of Feature Flags in Ruby on Rails with AppSignal
We'll set up feature flags in a Solidus storefront using Flipper and AppSignal's custom metrics.
See more
Five Things to Avoid in Ruby
We'll dive into five common Ruby mistakes and see how we can combat them.
See more

Tarun Singh
Tarun Singh is a software engineer and technical writer with 5+ years of experience creating developer-focused content on backend systems, APIs, and modern web development. He has published 800+ technical articles across major platforms and frequently writes deep-dive tutorials on developer tools, testing, AI, agentic tools, cloud, and infrastructure. Tarun is passionate about open source, developer education, and building reliable software systems.
All articles by Tarun SinghBecome our next author!
AppSignal monitors your apps
AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

