Writing / 2026

Using speedscope.app to Profile Ruby and Rails

A request that takes 900 ms tells us that something is slow, but not where the time went. A call-stack profiler supplies the missing data, and speedscope turns that data into an interactive flamegraph.

This guide is for Rails developers who already know how to run an application, add a gem, and execute a Rails runner script. It progresses through three levels:

  1. Quick diagnosis: profile one Rails request with rack-mini-profiler.
  2. Focused investigation: profile a service, script, or job with StackProf.
  3. Reliable analysis: choose the right sampling mode, reduce noise, and validate an optimization.

You do not need to learn every speedscope feature before using it. Start with a specific performance question, capture the smallest useful profile, and move to the more advanced techniques only when the first profile cannot answer that question.

The profiling stack

speedscope is a profile viewer, not a profiler. The tools have separate responsibilities:

  • StackProf samples Ruby call stacks.
  • rack-mini-profiler makes it convenient to profile a Rails request and uses StackProf for flamegraphs.
  • speedscope displays the recorded stacks and helps us explore them.

This guide uses the tools Rails developers are most likely to encounter, but speedscope is not limited to Ruby. It also supports profiles from rbspy, ruby-prof, browser developer tools, and profilers for other runtimes.

A sampling profiler periodically records the active call stack. It does not trace every method call, so its output is an estimate rather than an exact accounting of each invocation. Sampling usually has less overhead than tracing, but it is not free.

In a speedscope flamegraph:

  • vertical position represents stack depth;
  • a frame above another frame was called by it; and
  • width represents the frame’s weight in the profile.

The meaning of that weight depends on the recording mode. It can represent CPU samples, elapsed-time samples, or sampled allocations. A wide frame is worth investigating, but it does not necessarily indicate a frequently called method or prove that the method itself is the root cause.

Level 1: Profile a Rails request

Use this level when: one controller action or page is slow and you want a fast first look.

Add the profiling gems to the application if they are not already present:

# Gemfile
gem "rack-mini-profiler"
gem "stackprof"

After running bundle install, start the application and append ?pp=flamegraph to the request URL:

http://localhost:3000/reports/42?pp=flamegraph

rack-mini-profiler profiles that request and returns its speedscope flamegraph instead of the normal response.

For a JSON endpoint, XHR request, or another request whose response must remain intact, use:

?pp=async-flamegraph

The request completes normally and rack-mini-profiler stores the result for later viewing. The response also includes the viewer path in the X-MiniProfiler-Flamegraph-Path header.

Read the profile in three passes

speedscope offers three views, selected with the number keys 1, 2, and 3. Each answers a different question.

Pass 1: Sandwich — which methods stand out?

Start in Sandwich view. It presents a sortable function table and shows the callers and callees of the selected method.

  • Sort by self time to find methods that do substantial work directly.
  • Sort by total time to find methods whose complete subtree is expensive.
  • Select an application method and inspect what calls it and what it calls.

A Rails or gem method with high total time and low self time may only be coordinating expensive children. Follow its callees before deciding what to optimize.

Pass 2: Left Heavy — which call path dominates?

Left Heavy combines identical stacks and places the heaviest child first. Use it to answer, “Which route through the code accounts for most of this profile?”

Follow a wide branch from a controller, job, or service into its children. Prefer an application-owned frame that leads to the expensive work over a generic framework frame such as process_action.

Pass 3: Time Order — when did the work happen?

Time Order preserves the sequence in which stacks were sampled. Use it to separate phases such as loading records, transforming them, and serializing the response.

Zoom into the relevant phase instead of interpreting the entire request at once. Select a range in the minimap to narrow the profile, then double-click a frame to fit it in the viewport. The horizontal axis reflects sampled weight, so treat it as an execution sequence rather than a precise event timeline.

What this first profile can tell you

A request flamegraph can reveal that most observed work sits under serialization, a template partial, a calculation, or a database adapter. It cannot by itself explain every underlying cause.

For example, a wide database adapter frame in a wall-time profile indicates where the request waited. Use query logs and the database’s EXPLAIN (ANALYZE) to determine why the query was slow. The flamegraph identifies the path to investigate; it is not the final diagnosis.

One rack-mini-profiler detail matters here: normal SQL timings are not recorded during a flamegraph request, which keeps that timing instrumentation out of the sampled graph. Inspect the SQL list in an ordinary profiled request and inspect call stacks in a flamegraph request.

Level 2: Profile a focused operation with StackProf

Use this level when: you need to isolate a service object, background job, import, or script instead of profiling a complete web request.

A focused profile is usually easier to interpret because Rails boot, middleware, authentication, and response handling are absent. Put setup outside the profiling block and wrap only the operation related to the question.

# script/profile_report.rb
require "stackprof"

account = Account.find(42)
report = Reports::Monthly.new(account: account)

StackProf.run(
  mode: :cpu,
  raw: true,
  out: Rails.root.join("tmp/report.dump").to_s
) do
  report.render
end

Run it in the Rails environment:

bin/rails runner script/profile_report.rb

Then convert the native StackProf dump to JSON:

bundle exec stackprof tmp/report.dump --json > tmp/report.json

Open speedscope.app and drag tmp/report.json onto the page. The hosted viewer processes the profile in the browser rather than uploading it. speedscope’s StackProf importer requires StackProf 0.2.11 or newer, and raw: true preserves the samples needed to reconstruct the stacks.

You can also write importable JSON directly when keeping a native dump is unnecessary:

require "json"
require "stackprof"

profile = StackProf.run(mode: :wall, raw: true) do
  ImportCustomers.call
end

File.write(
  Rails.root.join("tmp/import-customers.json"),
  JSON.generate(profile)
)

Choose the mode before interpreting width

The mode should match the performance question, not merely the type of code being profiled.

CPU mode: where is Ruby computing?

StackProf.run(mode: :cpu, raw: true) { report.render }

Use CPU mode for calculations, parsing, rendering, and serialization. Waiting on the database, network, a lock, or sleep will not dominate this profile even when it dominates the user’s elapsed time.

Wall mode: where does elapsed time pass?

StackProf.run(mode: :wall, raw: true) { ImportCustomers.call }

Use wall mode for operations that mix Ruby work with database or network I/O. It is usually the better starting point for end-to-end requests and jobs.

Object mode: where are objects allocated?

StackProf.run(mode: :object, raw: true) { ExportCustomers.call }

In object mode, width represents sampled allocations—not elapsed time and not retained memory. Use it to locate allocation-heavy paths that may increase garbage-collection pressure. Use a memory profiler when the question is which objects remain in memory.

A practical rule is to begin with wall mode when the complaint is “users wait too long,” then capture a CPU profile if the wall profile points to Ruby computation. This avoids trying to answer an elapsed-time question with CPU-only evidence.

Level 3: Produce a profile you can trust

Use this level when: the graph is noisy, results change between runs, or you need evidence that an optimization worked.

1. State one question

“Why is the application slow?” is too broad. Prefer a question that determines both the boundary and the mode:

  • Why is report serialization CPU-heavy?
  • Where does this import wait on I/O?
  • Which path allocates objects while rendering this collection?

2. Exclude unrelated setup

Rails boot, constant loading, connection establishment, and cold caches can overwhelm a short recording. Warm up first and prepare records before the block unless startup or record lookup is part of the question.

report = Reports::Monthly.new(account: Account.find(42))
report.render # warm caches and lazy initialization

StackProf.run(mode: :cpu, raw: true, out: "tmp/report.dump") do
  10.times { report.render }
end

Repeating a short operation can collect more samples, but only do so when repetition preserves realistic behavior. A cached second call may exercise a different path from the first.

3. Use representative inputs

Profile production-like data volume and shape. A report with ten rows may use a different algorithm, query pattern, or allocation profile from one with 100,000 rows. Keep the inputs consistent when comparing before and after profiles.

4. Check self time and total time together

A frame’s total time includes samples in its descendants. Its self time includes samples where that frame is the top of the observed stack.

This distinction prevents a common mistake: optimizing a wrapper because it is wide even though almost all of its weight belongs to a child. Use Sandwich view to compare the two, then use Left Heavy to locate the responsible branch.

5. Repeat the measurement

Run the workload several times. Garbage collection, cache state, database state, and machine load can change one run. StackProf also reports recorded and missed samples; a high number of missed samples lowers confidence in the profile’s shape.

The interval option changes sampling frequency. A smaller interval can provide more detail, but also increases overhead and profile size. Begin with the default and adjust it only when the workload is too short or the profile lacks enough samples.

6. Validate outside the flamegraph

After making one change:

  1. run the same workload with the same input;
  2. capture another profile;
  3. compare the relevant branch, self time, and total time; and
  4. measure the user-facing duration without the profiler.

A branch becoming narrower does not guarantee that the request became faster. The work may have moved elsewhere, and profiling itself affects execution.

How speedscope keeps large profiles explorable

StackProf’s sampling reduces recording overhead by observing the current call stack periodically instead of tracing every method call. That efficiency belongs to the profiler; speedscope contributes a different kind of efficiency when exploring the result.

speedscope parses a profile into reusable in-memory representations. Left Heavy merges identical paths, turning many interleaved samples into weighted branches. Its flamechart uses batched drawing rather than one HTML element per frame, and long lists render only rows in or near the viewport. Searching, sorting, panning, and zooming then happen locally in the browser.

Large profiles can still require noticeable time and memory to parse. The practical goal is responsive exploration, not zero-cost profiling, so it is still best to capture the smallest representative workload that answers the question.

Advanced Rails configuration

rack-mini-profiler’s defaults are suitable for an initial request profile. Adjust them only when the default recording cannot answer the question:

# config/initializers/mini_profiler.rb
Rack::MiniProfiler.config.flamegraph_mode = :wall
Rack::MiniProfiler.config.flamegraph_sample_rate = 0.5 # milliseconds
Rack::MiniProfiler.config.flamegraph_ignore_gc = false

Wall mode is the default because web requests often wait on I/O. Switch to CPU mode when you specifically need to isolate processor work. Keeping garbage-collection frames visible can expose allocation pressure; hiding them reduces visual noise but removes that context.

Do not expose profiling controls to every production user. Profiles can reveal class names, method names, file paths, and application behavior. rack-mini-profiler supports explicit authorization, and multi-server deployments require shared storage such as Redis or Memcache.

Beyond Rails: Profile frontend JavaScript

The same three-view workflow applies to browser JavaScript. In Chrome DevTools, open the Performance panel, start recording, reproduce the slow interaction, stop recording, and save the profile. Drag the resulting JSON file into speedscope.app.

Use Time Order to isolate the interaction, Left Heavy to find the JavaScript call paths consuming the most sampled CPU time, and Sandwich to inspect an expensive function’s callers and callees. This is useful for long-running event handlers, repeated renders, and CPU-heavy parsing or transformation.

Return to the browser’s Performance panel for network activity, layout, painting, and Web Vitals: speedscope focuses on call stacks rather than the browser’s complete performance timeline. speedscope also supports profiles exported by Firefox and Safari.

Common interpretation mistakes

  • Width is not call count. One slow call can remain on the stack across many samples, while many fast calls can occur between samples.
  • A sample is not an exact timer. Small differences between frames or runs may be sampling noise.
  • High total time does not mean high self time. Inspect children before changing a wide method.
  • CPU mode does not explain I/O waits. Use wall mode for end-to-end latency.
  • Object mode does not measure retained memory. It shows sampled allocations.
  • A library frame is not automatically the cause. Follow callers and callees to understand how application code reached it.
  • One run is weak evidence. Repeat the workload and verify the result with normal timing.

Keep generated dumps under tmp/, out of version control, and inspect them before sharing. Useful speedscope shortcuts are Cmd/Ctrl+F to search for a frame, 0 to reset zoom, r to collapse recursion, and the arrow keys or w/a/s/d to pan.

A repeatable workflow

For most intermediate Rails investigations, this is enough:

  1. Define one narrow performance question.
  2. Start with a request flamegraph or a focused StackProf block.
  3. Select wall, CPU, or object mode to match the question.
  4. Use Sandwich to find candidates, Left Heavy to follow dominant paths, and Time Order to isolate phases.
  5. Read the relevant application code and confirm the suspected cause with a companion tool when necessary.
  6. Change one thing, profile again, and measure elapsed time without profiling.

Treat speedscope as a map rather than a verdict. It shows where sampled work occurred; a reliable optimization still depends on representative inputs, the correct mode, a testable hypothesis, and before-and-after measurements.

References