Back to Scaling Ruby Applications guides

How to Profile Ruby Code

Stanley Ulili
Updated on September 24, 2025

Ruby makes it easy to write code that works, but harder to write code that works fast. Performance bottlenecks can lurk anywhere in your application, and trying to guess where they are usually wastes more time than it saves.

The Ruby ecosystem includes several profiling tools that show you exactly where your program spends its time. These tools track method calls, measure execution duration, and reveal the relationships between different parts of your code.

This article walks you through Ruby profiling from basic timing to advanced performance analysis techniques.

Prerequisites

Before continuing, make sure you have a recent version of Ruby (version 3.0 or higher) installed on your local machine. This guide assumes you're already comfortable with basic Ruby concepts and gem management.

Step 1 — Getting started with Ruby profiling

For the best learning experience, set up a fresh Ruby project to experiment directly with the concepts introduced in this tutorial.

Begin by creating a new directory and setting up your project:

 
mkdir ruby-profiling && cd ruby-profiling
 
bundle init

Let's start with a common recursive algorithm you'll use throughout this article to demonstrate different profiling techniques - the Fibonacci sequence.

Create a file named main.rb with the following content:

main.rb
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
  result = fibonacci(30)
  puts "Fibonacci(30) = #{result}"
end

This method calculates the n-th Fibonacci number recursively. While simple to understand, this implementation has exponential time complexity - it recalculates the same Fibonacci numbers repeatedly, making it inefficient for larger values of n.

Let's see just how inefficient it is with some manual timing:

main.rb
require 'time'
def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end if __FILE__ == $0
start_time = Time.now
result = fibonacci(30)
end_time = Time.now
puts "Fibonacci(30) = #{result}"
puts "Time taken: #{(end_time - start_time).round(4)} seconds"
end

Here, you use Ruby's built-in Time class to measure manually precisely how long the recursive Fibonacci calculation takes.

Run your script with the following command:

 
ruby main.rb

You'll see output like:

Output
Fibonacci(30) = 832040
Time taken: 0.0785 seconds

While this basic timing approach tells us how long the method takes to run, it doesn't provide insights into what's happening inside it.

For complex applications with many methods, measuring total execution time doesn't help identify bottlenecks. This is where profiling comes in.

Step 2 — Basic profiling with the built-in ruby-prof gem

While manual timing gives us a general idea of overall execution time, it has several limitations:

  • It only shows total execution time, not where time is spent within the method
  • No call count information. We can't see how many times each method was called
  • We don't see the relationships between method calls
  • You need to add timing code around every method you want to measure

This is where Ruby's profiling tools become invaluable. The ruby-prof gem provides detailed insights without requiring you to modify your code extensively.

Let's use ruby-prof to analyze our Fibonacci method. First, add it to your Gemfile:

Gemfile
source "https://rubygems.org"

gem "ruby-prof", "~> 1.7"

Install the gem:

 
bundle install

Now let's use ruby-prof to analyze our Fibonacci method in a new version of main.rb:

main.rb
require 'ruby-prof'
def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end if __FILE__ == $0
# Create a new profiler instance
profiler = RubyProf::Profile.new
# Start profiling
profiler.start
# Run the code we want to profile
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
# Stop profiling and get results
profiling_result = profiler.stop
# Print a flat profile to stdout
printer = RubyProf::FlatPrinter.new(profiling_result)
printer.print(STDOUT, min_percent: 1)
end

In the highlighted blocks, you first require the ruby-prof gem for gathering detailed performance data. Next, you create a profiler instance using RubyProf::Profile.new and start capturing execution metrics with profiler.start. With profiling active, you run the fibonacci(30) method to capture detailed execution data. Once the method finishes executing, you stop the profiler with profiler.stop.

Finally, you use RubyProf::FlatPrinter to process and format the profiling data, outputting methods that consume at least 1% of execution time to quickly identify the most time-consuming parts of your code.

Run this script:

 
ruby main.rb

You should observe output similar to the following:

Output
Fibonacci(30) = 832040
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.707144
Sort by: self_time
 %self      total      self      wait     child     calls  name                           location
 71.76      1.707     1.225     0.000     0.482  2692537  *Object#fibonacci               main.rb:3
 11.70      0.200     0.200     0.000     0.000  2692537   Integer#<=                     
 10.46      0.179     0.179     0.000     0.000  2692536   Integer#-                      
  6.08      0.104     0.104     0.000     0.000  1346268   Integer#+                      

This output reveals something striking - our fibonacci method was called 2,692,537 times to calculate the 30th Fibonacci number!

The profiling data shows more than just our custom method. Notice that the basic arithmetic operations (Integer#<=, Integer#-, and Integer#+) also appear in the profile. This happens because our recursive implementation triggers millions of these operations - each method call needs to check if n <= 1, subtract values for the recursive calls, and add the results together.

Let's understand what each column in the output means:

  • %self: The percentage of time spent by this method relative to the total time in the entire program
  • total: The total time spent by this method and its children
  • self: The time spent by this method
  • wait: The time this method spent waiting for other threads
  • child: The time spent by this method's children
  • calls: The number of times this method was called
  • name: The name of the method
  • location: The location of the method

The asterisk (*) next to *Object#fibonacci indicates this is a recursively called method. Ruby-prof tracks both the total number of calls and marks methods that call themselves.

With this information, you can immediately identify algorithm inefficiencies, prioritize your efforts, and make data-driven decisions about where to focus your performance tuning work.

The profile data clearly shows that our naive recursive implementation is extremely inefficient due to the millions of redundant method calls - not just to our fibonacci method, but also to basic Ruby operations.

However, working with the profiling data this way can still be cumbersome, especially for larger applications. Let's make profiling easier with a reusable solution.

Step 3 — Creating a reusable profiling decorator

While directly using ruby-prof works well for one-off profiling, you'll often want to profile multiple methods in real projects. Instead of repeating the same profiling code, let's create a reusable decorator that can be applied to any method.

Create a new file named profiler.rb:

profiler.rb
require 'ruby-prof'

module Profiler
  def self.profile(description = nil)
    profiler = RubyProf::Profile.new
    profiler.start

    # Call the provided block
    result = yield

    # Stop profiling
    profiling_result = profiler.stop

    # Format and print results
    puts "\n" + "="*50
    puts "Profile: #{description}" if description
    puts "="*50

    printer = RubyProf::FlatPrinter.new(profiling_result)
    printer.print(STDOUT, min_percent: 1)

    result
  end
end

This module wraps any block of code with profiling logic, making it easy to profile any part of your application. The profile method accepts an optional description and a block, then measures the block's execution while providing formatted output.

For our Fibonacci example, you can now wrap the calculation in a clean profiling block:

main.rb
require_relative 'profiler'
def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end
if __FILE__ == $0
Profiler.profile("Fibonacci calculation") do
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
end
end

Now you can add profiling to any code block with a single method call. Run this script:

 
ruby main.rb

You'll see the same detailed profiling information as before, but now formatted with a clear header:

Screenshot of the output

Output
Fibonacci(30) = 832040

==================================================
Profile: Fibonacci calculation
==================================================
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.675167
Sort by: self_time

 %self      total      self      wait     child     calls  name                           location
 72.17      1.675     1.209     0.000     0.466  2692537  *Object#fibonacci               main.rb:3
 11.44      0.192     0.192     0.000     0.000  2692537   Integer#<=                     
 10.38      0.174     0.174     0.000     0.000  2692536   Integer#-                      
  6.01      0.101     0.101     0.000     0.000  1346268   Integer#+                      

You now have a reusable profiling tool that can be applied to any code block with a single line. This approach is much more maintainable for larger projects where you need to profile different parts of your codebase.

Step 4 — Saving profile data to files

To analyze profiling data more thoroughly or share it with team members, we should enhance our profiler to save data to files. Let's modify our profiler.rb:

profiler.rb
require 'ruby-prof'

module Profiler
def self.profile(description = nil, output_file = nil)
profiler = RubyProf::Profile.new
profiler.start
# Call the provided block
result = yield
# Stop profiling
profiling_result = profiler.stop
# Print formatted results to console
puts "\n" + "="*50
puts "Profile: #{description}" if description
puts "="*50
printer = RubyProf::FlatPrinter.new(profiling_result)
printer.print(STDOUT, min_percent: 1)
# Save to file if requested
if output_file
File.open(output_file, 'w') do |file|
printer = RubyProf::FlatPrinter.new(profiling_result)
printer.print(file)
end
puts "\nProfile data saved to #{output_file}"
end
result
end
end

Here, you've updated the profile method to include an optional output_file parameter, enabling you to specify a file location for saving profiling results.

You've also introduced new file-saving functionality. The added if output_file block writes profiling data to the specified file using RubyProf::FlatPrinter.print(). Afterward, a confirmation message informs you that the file was created successfully.

This enhancement allows you to save profiling data in a text format for later analysis. Now, let's update your main.rb file to take advantage of this new feature:

main.rb
require_relative 'profiler'

def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

if __FILE__ == $0
Profiler.profile("Fibonacci calculation", "fibonacci.prof") do
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
end
end

In the highlighted line, you've used the enhanced profile method with the new output_file argument.

This modification instructs the profiler to save profiling results directly into a text file named fibonacci.prof

Now, run your script to generate and save the profiling data:

 
ruby main.rb

After the script runs, you'll receive a confirmation message indicating the profile data was successfully saved to this file:

Output
==================================================
Profile: Fibonacci calculation
==================================================
Measure Mode: wall_time
Thread ID: 24
Fiber ID: 16
Total: 1.729846
Sort by: self_time

 %self      total      self      wait     child     calls  name                           location
 71.97      1.730     1.245     0.000     0.485  2692537  *Object#fibonacci               main.rb:3
 11.69      0.202     0.202     0.000     0.000  2692537   Integer#<=                     
 10.29      0.178     0.178     0.000     0.000  2692536   Integer#-                      
  6.05      0.105     0.105     0.000     0.000  1346268   Integer#+                      

* recursively called methods

...
Profile data saved to fibonacci.prof

You can verify the file exists:

 
ls -l fibonacci.prof
Output
-rw-r--r--  1 stanley  staff  1234 Sep 24 15:30 fibonacci.prof

This .prof file contains all your profiling data in a text format. The benefit of saving to a file is that you can:

  1. Analyze profiles offline without re-running the code
  2. Compare profiles from different runs
  3. Share profile data with team members
  4. Use external tools to visualize and analyze the data

Saving profile data becomes especially valuable when profiling production systems or long-running processes where you can't quickly examine the immediate console output.

Step 5 — Visualizing profile data with stackprof

With the profile data saved to a file, you can visualize it using specialized tools. One of the most popular options is stackprof, which creates interactive graphical representations of Ruby profiling data.

First, let's switch to using stackprof for better visualization support. Add it to your Gemfile:

Gemfile
source "https://rubygems.org"

gem "ruby-prof", "~> 1.7"
gem "stackprof", "~> 0.2"
 
bundle install

Now let's create a version that uses stackprof for data collection and visualization:

main.rb
require 'stackprof'
def fibonacci(n) return n if n <= 1 fibonacci(n - 1) + fibonacci(n - 2) end if __FILE__ == $0
# Profile using stackprof with raw data for flamegraphs
StackProf.run(mode: :cpu, raw: true, out: 'fibonacci_stackprof.dump') do
result = fibonacci(30)
puts "Fibonacci(30) = #{result}"
end
end

This creates a binary dump file that can be analyzed with stackprof's command-line tools. The raw: true option is essential for generating visual flamegraphs.

Run this script:

 
ruby main.rb

You'll see:

Output
Fibonacci(30) = 832040

Now you can analyze the generated dump file using stackprof's built-in report generator:

 
bundle exec stackprof fibonacci_stackprof.dump --text

This command generates a text-based report showing your profiling data:

Output
==================================
  Mode: cpu(1000)
  Samples: 36 (0.00% miss rate)
  GC: 0 (0.00%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
        35  (97.2%)          35  (97.2%)     Object#fibonacci
         1   (2.8%)           1   (2.8%)     IO#write
        36 (100.0%)           0   (0.0%)     block in <main>
        36 (100.0%)           0   (0.0%)     StackProf.run
        36 (100.0%)           0   (0.0%)     <main>
         1   (2.8%)           0   (0.0%)     IO#puts
         1   (2.8%)           0   (0.0%)     Kernel#puts

For more detailed analysis, you can generate an interactive D3 flamegraph by redirecting the output to an HTML file:

 
bundle exec stackprof fibonacci_stackprof.dump --d3-flamegraph > fibonacci_flamegraph.html

Now open the generated HTML file in your browser:

Screenshot of the Fibonacci framegraph in the browser

The flamegraph displays an interactive visualization where you can hover over different sections to see method details, click to zoom into specific call paths, and use the search functionality to find particular methods. The width of each rectangle represents the proportion of execution time, and the vertical stacking shows the call depth.

For the recursive Fibonacci function, you'll see a deep tower of identical Object#fibonacci calls, clearly illustrating why this algorithm is so inefficient. Each level represents another recursive call, and you can visually see how the same calculations are repeated millions of times.

The interactive features include a reset zoom button, clear search functionality, and detailed information panels that update as you explore different parts of the call tree. This visual representation makes it immediately obvious where performance bottlenecks occur and how method calls relate to each other during execution.

Final thoughts

Profiling removes the guesswork from Ruby performance tuning. Tools like ruby-prof and stackprof reveal where your application spends its time and highlight the methods most worth optimizing. With these insights integrated into your workflow, performance improvements become a focused, data-driven process instead of trial and error.

Got an article suggestion? Let us know
Next article
Top 10 Ruby on Rails Alternatives for Web Development
Discover the top 10 Ruby on Rails alternatives. Compare Django, Laravel, Express.js, Spring Boot & more with detailed pros, cons & features.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.