πŸ› οΈ Classic Performance Debugging Problems in Rails Apps β€” Part 2: Tools & Fixes

In Part 1, we learned how to spot bottlenecks using Rails logs.
Now let’s go deeper β€” using profiling tools, custom logging, and real-world fixes (including our Flipper case).

🧰 Profiling Tools for Rails Developers

1. Rack Mini Profiler πŸ“Š

Rack Mini Profiler is the go-to gem for spotting slow DB queries and views.

Add it to your Gemfile (development & staging only):

group :development do
  gem 'rack-mini-profiler'
end

Then run:

bundle install

When you load a page, you’ll see a little timing panel in the top-left corner:

  • Total time per request.
  • SQL queries count & time.
  • Which queries are repeated.
  • View rendering breakdown.

This makes N+1 queries immediately visible.


2. Bullet πŸ”« (for N+1 Queries)

Add to Gemfile:

group :development do
  gem 'bullet'
end

Config in config/environments/development.rb:

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
end

Now, if you forget to eager load (includes), Bullet will warn:

N+1 query detected: Review => Product
Add to your query: .includes(:product)


3. Custom Logging for SQL Queries πŸ“

Sometimes you need to trace where in your code a slow query is triggered.
Rails lets you hook into ActiveRecord logging:

# config/initializers/query_tracer.rb
ActiveSupport::Notifications.subscribe("sql.active_record") do |_, start, finish, _, payload|
  duration = (finish - start) * 1000
  if duration > 100 # log queries > 100ms
    Rails.logger.warn "SLOW QUERY (#{duration.round(1)}ms): #{payload[:sql]}"
    Rails.logger.warn caller.select { |line| line.include?(Rails.root.to_s) }.first(5).join("\n")
  end
end

This logs:

  • The query.
  • Execution time.
  • Stack trace (where in Rails code it was triggered).

⚑ Real Example: Fixing Flipper Performance

In Part 1, we saw Flipper queries running 38 times per page:

SELECT "flipper_features"."key" AS feature_key,
       "flipper_gates"."key",
       "flipper_gates"."value"
FROM "flipper_features"
LEFT OUTER JOIN "flipper_gates"
ON "flipper_features"."key" = "flipper_gates"."feature_key"

Problem πŸ”₯

Each Flipper.enabled?(:feature_name, user) call hit the DB.
With dozens of flags per request β†’ repeated queries β†’ 6s page loads.

Solution βœ…: Redis Caching

Flipper supports caching with Redis.

# config/initializers/flipper.rb
require 'flipper/adapters/active_record'
require 'flipper/adapters/redis'
require 'flipper/adapters/operation_logger'

flipper_db_adapter = Flipper::Adapters::ActiveRecord.new
flipper_redis_adapter = Flipper::Adapters::Redis.new(Redis.new)

# wrap with memory cache to avoid repeat queries
flipper_caching_adapter = Flipper::Adapters::Cache.new(
  flipper_db_adapter,
  cache: flipper_redis_adapter,
  expires_in: 5.minutes
)

Flipper.configure do |config|
  config.default = Flipper.new(flipper_caching_adapter)
end

Now:

  • First request β†’ fetches from DB, writes to Redis.
  • Next requests β†’ served from Redis/memory.
  • Page load dropped from 6s β†’ <500ms πŸŽ‰.

Extra: Inspecting Redis Keys πŸ”

Check what’s being cached:

Rails.cache.redis.keys("*flipper*")

Or, if using connection pooling:

Rails.cache.redis.with { |r| r.keys("*flipper*") }


πŸ—οΈ Other Common Fixes

1. Add Missing Indexes

If you see slow queries with filters:

SELECT * FROM orders WHERE user_id = 123

Fix:

rails generate migration add_index_to_orders_user_id
rails db:migrate

2. Eager Load Associations

Instead of:

@orders = current_user.orders
@orders.each do |order|
  puts order.user.email
end

Fix N+1:

@orders = current_user.orders.includes(:user)

3. Memoization / Request Caching

If you call the same method multiple times per request:

def expensive_query
  User.where(active: true).to_a
end

Fix:

def expensive_query
  @expensive_query ||= User.where(active: true).to_a
end

πŸ“Œ Summary of Part 2

In this part, we covered:

  • Using tools: Rack Mini Profiler, Bullet, and custom logging.
  • A real-world Flipper caching fix (DB β†’ Redis).
  • Other fixes: indexes, eager loading, memoization.

Together, these tools + fixes turn performance debugging from guesswork into a repeatable workflow.

πŸ“Œ In Part 3, we’ll go even deeper into advanced techniques:

  • EXPLAIN ANALYZE in Postgres.
  • Profiling memory allocations with stackprof.
  • Using Skylight or Datadog in production.

Unknown's avatar

Author: Abhilash

Hi, I’m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, I’ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React. Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Let’s explore the ever-evolving world of web development together!

One thought on “πŸ› οΈ Classic Performance Debugging Problems in Rails Apps β€” Part 2: Tools & Fixes”

Leave a comment