RUBY ON RAILS · 12 MIN READ ·

Rack Mini Profiler: Performance Profiling for Rails in Development and Production

Rack Mini Profiler for Rails: profile SQL queries, partials, memory, and GC in development and production. Find N+1s, slow views, and hidden bottlenecks fast.

Rack Mini Profiler: Performance Profiling for Rails in Development and Production

The page loaded in three seconds. Not consistently — sometimes two, occasionally four. No errors, nothing obvious in the logs. The application had been running in production for eighteen months and this particular admin dashboard had quietly become unusable while the team was heads-down shipping features.

I asked if they had rack-mini-profiler installed. They did not. I added it, opened the page, and within thirty seconds we had the answer: 94 SQL queries on a single page load. A classic N+1 on a has_many through: association, buried in a partial that had grown to touch six different models. The fix was a two-line eager load. The fix took an afternoon. The profiler took thirty seconds.

After nineteen years of Rails, rack-mini-profiler is still the first tool I reach for when someone tells me a page is slow.

What Rack Mini Profiler Shows You

Rack-mini-profiler is a Rack middleware that injects a small profiling toolbar into your HTML responses. The toolbar shows total request time, time spent in Rails rendering, and time spent in the database — broken down by SQL query count and duration.

That sounds simple. The power is in the details. The toolbar is persistent, per-page, and appears in the context of the page you are debugging rather than in a separate tool that requires you to reproduce the scenario elsewhere. You see the numbers while using the application normally.

Click the toolbar and you get a query-by-query breakdown: each SQL statement, its duration, the calling stack trace, and how many times it ran. That stack trace is how you find the source line driving your N+1 — you do not need to grep for eager load candidates or read query plans. The profiler tells you exactly which line of Ruby is issuing each query.

Installation and Configuration

Add to your Gemfile:

gem "rack-mini-profiler"
gem "memory_profiler"  # enables memory profiling
gem "stackprof"        # enables flamegraph support

Rack-mini-profiler auto-enables itself in development and adds the toolbar to every HTML response. In production and staging it is silent by default — the toolbar appears only for authorized requests.

Basic configuration in an initializer:

# config/initializers/rack_profiler.rb
if Rails.env.development?
  Rack::MiniProfiler.config.start_hidden = false
  Rack::MiniProfiler.config.position     = "bottom-right"
  Rack::MiniProfiler.config.skip_paths   = ["/assets", "/cable"]
end

skip_paths prevents the profiler from running on asset requests and Action Cable pings. Those clutter the toolbar history without being useful for application profiling.

For Rails apps using Propshaft or Sprockets, the gem integrates automatically. No manual middleware insertion needed — it detects the Rack stack and inserts itself at the right position.

Finding N+1 Queries with Rack Mini Profiler

This is the daily use case. The SQL tab inside rack-mini-profiler shows every query grouped by caller. An N+1 appears as a repeating query with a count greater than one — usually with a small per-query duration that accumulates into a significant total.

The workflow:

  1. Load the page
  2. Click the speed badge in the corner
  3. Switch to the SQL tab
  4. Look for queries with a count of N where N is proportional to the number of records on the page

If you have 20 orders and you see SELECT * FROM line_items WHERE order_id = ? listed 20 times, you have found your N+1. The stack trace for each query instance points to the exact partial or method that called it.

The fix:

# Before: N+1 — one query per order
@orders = Order.where(user: current_user).order(created_at: :desc)

# After: eager load line_items in a single query
@orders = Order.where(user: current_user)
               .includes(:line_items)
               .order(created_at: :desc)

The N+1 queries guide covers Bullet integration and strict loading alongside rack-mini-profiler. Use the profiler to find N+1s interactively; use Bullet to catch regressions automatically in CI. They are complementary tools, not alternatives.

Memory Profiling

With memory_profiler installed, append ?pp=profile-memory to any URL to get a full allocation report for that request:

http://localhost:3000/admin/orders?pp=profile-memory

The response is a text report showing allocated objects by gem, by file, and by line number. It is verbose, but the Total allocated: summary at the top is what you start with. A typical Rails request allocates somewhere between 50,000 and 300,000 objects. Requests that allocate several million objects are worth investigating.

The report groups allocations by gem, so you can see whether pressure is coming from your application code, from ActiveRecord, from your templating layer, or from a third-party gem. A report showing 80% of allocations from a serializer gem is actionable: the gem is creating excessive intermediate objects, and either it needs replacement or the serialization needs caching.

For deeper investigation of production memory growth, the Ruby GC tuning guide covers GC.stat analysis, heap size configuration, and ObjectSpace — the right tools once you have identified where the pressure originates.

Flamegraphs

Append ?pp=flamegraph to get a Speedscope-compatible flamegraph of the full request call stack:

http://localhost:3000/admin/orders?pp=flamegraph

The flamegraph renders in-browser using Speedscope. Each horizontal block is a stack frame; its width represents time. Tall stacks you did not expect, or wide blocks in unexpected places, tell you what the process is actually spending time on rather than what you assume it is spending time on.

The flamegraph is the right tool when SQL query count looks reasonable but the page is still slow. If database time is under 50ms but total request time is 800ms, the remaining 750ms is in Ruby execution — serialization, view rendering, object allocation, regular expressions in template helpers, or a gem doing something expensive you have never inspected. The flamegraph surfaces it without guesswork.

stackprof must be installed for ?pp=flamegraph to work. The request must complete in under a few minutes for the profiler to generate the output correctly.

Enabling Rack Mini Profiler in Staging and Production

The toolbar is a security risk if visible to unauthenticated users — it exposes query structure, partial names, and timing data that an attacker can use to understand your application’s internals. The correct pattern gates it behind your existing authentication:

# config/initializers/rack_profiler.rb
Rack::MiniProfiler.config.authorization_mode = :allowlist
# app/controllers/admin/base_controller.rb
class Admin::BaseController < ApplicationController
  before_action :authorize_profiler

  private

  def authorize_profiler
    Rack::MiniProfiler.authorize_request if current_user&.admin?
  end
end

With this in place, visiting any admin URL as an admin activates the profiler toolbar for your session. Non-admin requests see nothing. This is safe to leave in production permanently — negligible overhead for profiled sessions, zero overhead for everything else.

For one-off investigations on specific production pages, the ?pp=env and ?pp=profile-gc parameters return plain-text responses rather than injecting a toolbar. You can retrieve them with curl from an authenticated session:

curl -b "session_cookie=your_session_value" \
  "https://yourapp.com/admin/slow-page?pp=env"

Combining with EXPLAIN ANALYZE

Rack-mini-profiler shows which queries are slow and how many times they run. It does not explain why an individual query is slow at the database level — for that, you need EXPLAIN ANALYZE.

The workflow:

  1. Identify slow queries in the profiler SQL tab (anything over 50ms is worth investigating)
  2. Copy the query text
  3. Run it with EXPLAIN ANALYZE in psql or your database client
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders
WHERE user_id = 42 AND status = 'pending'
ORDER BY created_at DESC;

The Postgres EXPLAIN ANALYZE guide covers reading query plans, spotting sequential scans, and choosing the right index type. The combination — rack-mini-profiler to identify which query to investigate, EXPLAIN ANALYZE to understand why it is slow — covers nearly every production database performance problem I encounter.

Profiling Background Jobs

Rack middleware only intercepts HTTP requests. It does not profile Solid Queue or Sidekiq workers. For jobs, use stackprof directly:

# app/jobs/process_orders_job.rb
class ProcessOrdersJob < ApplicationJob
  def perform
    result = StackProf.run(mode: :wall, raw: true) do
      Order.pending.find_each do |order|
        ProcessOrder.call(order)
      end
    end

    report = StackProf::Report.new(result)
    report.print_text(false, 50) # top 50 frames by self time
  end
end

Run this manually from the Rails console in development or staging. The output is a plain-text table of the heaviest stack frames — the job equivalent of a flamegraph, without the browser rendering.

For continuous production job performance monitoring, OpenTelemetry instrumentation is the better investment: spans on every job execution with database time and queue wait time tracked automatically across all workers, not just the ones you happen to profile manually.

FAQ

Does rack-mini-profiler work with Turbo and Hotwire?

Yes, with a small wrinkle. Turbo Drive intercepts full-page navigations, so the toolbar persists between pages without a full reload — which means you can profile across a multi-step flow without losing earlier results. Turbo Frame requests are separate HTTP requests and appear as individual entries in the toolbar dropdown history. If a Turbo Frame’s partial is slow, its profile is the most recent entry in the history after you trigger the frame. Navigate to the frame, then check the dropdown — the entry will be there.

Is it safe to run rack-mini-profiler in production?

With authorization enabled: yes. Without authorization: no. The SQL output reveals your schema, query patterns, and table names in enough detail to inform a targeted attack. Use the allowlist authorization mode and gate access behind admin authentication as shown above. The overhead for non-profiled requests is a single middleware check — unmeasurable in production traffic.

How do I profile a slow action that requires POST data?

The ?pp=flamegraph and ?pp=profile-memory parameters work on POST requests. Append them as query parameters to the form’s action URL, or to the fetch URL if the action is called via JavaScript. The profiler attaches to server-side processing regardless of HTTP method.

How does rack-mini-profiler compare to APM services like Skylight or Scout?

They serve different purposes. Rack-mini-profiler is a per-request debugging tool — interactive, immediate, with full SQL and stack detail for a single request. Skylight and Scout APM are production monitoring services that aggregate performance data across thousands of requests over time and surface slow endpoints, regressions, and trends at the fleet level. Use rack-mini-profiler to diagnose specific slow pages during development; use an APM service to know which pages need diagnosing in the first place. They are not alternatives — the best-run Rails apps I have worked on use both.

Chasing a performance problem that does not show up cleanly in your logs? TTB Software has spent nineteen years profiling Rails applications in development and production. We know which numbers to look at first and how to get from a slow page to a committed fix in a single session.

#rack-mini-profiler-rails #rails-performance-profiling #rails-sql-query-profiling #rails-memory-profiler #rails-flamegraph-stackprof #rails-development-performance-tools

Related Articles

Last section. Then please call.

It's a phone call. That's the worst it can get.

No discovery deck. No 45-minute "qualification" call. 30 minutes, your problem, my opinion. If we're a fit, you'll know by minute 12.

Direct line — answered by Roger
+31 6 5123 6132
Mon–Fri, 09:00–18:00 CET · Currently available

OR
info@ttb.software