More at rubyonrails.org:

Debugging Rails Applications

This guide covers the tools and techniques for debugging Rails applications.

After reading this guide, you will know:

  • How Rails' logging machinery works.
  • How to use Ruby's debug gem.
  • The Rails helper methods which aid with debugging.
  • How to inject a Ruby console in your view.

1. Introduction

Debugging is the process of finding the cause, and potential solutions or workarounds to software defects.

There are several strategies that can be used depending on the nature of the bug. Using a debugger, you can step through the code line-by-line to ensure statements execute correctly and verify the application's state. Inspecting logs can show which database queries are made, which views are rendered, and how long different statements take to execute. If your application is using too much memory, you can use instrumentation tools to check how your application allocates memory.

In this guide, we'll discuss some common tools, techniques, and approaches to debugging.

2. Debugging Using the Rails Logger

Writing to the logs is a useful technique to trace the application while it is running. The most basic way to use logs as a debugging tool is to add puts statements in your code:

class Search
  def initialize(params)
    @id = SecureRandom.hex(10)
    @params = params
  end

  def run
    puts "Starting search (#{@id}) at #{Time.zone.now}"
    # ...
    puts "Finshed search (#{@id}) at #{Time.zone.now}"
  end
end

You may wish to use p or pp instead of puts to log debug statements. p prints the raw object passed to it by calling inspect on it, rather than to_s which is what puts calls. It also returns the supplied object, contrasting with puts which always returns nil.

pp offers better log formatting than p by pretty printing objects such as hashes and arrays in a more human-readable format.

puts writes statements to STDOUT only. It offers no configuration options to enable or disable certain log statements, or write to multiple destinations. In a production setting, more control over logging is required. As such, Rails provides its own logger (ActiveSupport::Logger) which offers several features to control logging behavior.

Access the Rails logger using Rails.logger. In development and test environments, it writes logs to the file log/<environment>.log. In development, it also writes to STDOUT, and in production it writes ONLY to STDOUT by default. Log destinations for each environment can be configured.

Here's an example of writing messages using the Rails logger:

class Search
  def initialize(params)
    @id = SecureRandom.hex(10)
    @params = params
  end

  def run
    Rails.logger.debug { "Starting search (#{@id}) at #{Time.zone.now}" }
    # ...
    Rails.logger.debug { "Finshed search (#{@id}) at #{Time.zone.now}" }
  end
end

In the next sections, we'll dig deeper into the features of the Rails logger.

2.1. Log Levels

Every message logged by the Rails logger must be assigned a level. The available log levels and their corresponding integer values are:

:debug 0
:info 1
:warn 2
:error 3
:fatal 4
:unknown 5

A message is only written to the output if its level is equal to or higher than the configured log level. Rails.logger.level returns the current log level.

The default Rails log level is :debug in development and :info in production. You can set the log level for an environment in the application config:

# config/environments/<environment>.rb

config.log_level = :warn

Logging has a small performance overhead. Use log levels to control the verbosity of your logs.

2.2. Logging Messages

To write a log message, call logger.(debug|info|warn|error|fatal) method from anywhere in your app:

logger.debug  { "Person attributes: #{@person.attributes.inspect}" }
logger.info   { "Processing the request..." }
logger.fatal  { "Terminating application, raised unrecoverable error!!!" }

Where logger isn't directly available, such as in a plain Ruby class that doesn't inherit from any Rails classes (sometimes called a PORO, for Plain Old Ruby Object), use Rails.logger:

class Search
  def initialize(params)
    @params = params
  end

  def run
    Rails.logger.info { "Running search with params: #{@params}" }
    # ...
  end
end

Always pass a block to logger#<level> instead of a string. This way, the message will only be evaluated if its log level is active. See the Impact of Logs on Performance section for more information.

Here's an example of a method instrumented with extra logging:

class PostsController < ApplicationController
  # ...

  def create
    @post = Post.new(post_params)
    logger.debug { "New post instantiated: #{@post.attributes.inspect}" }

    if @post.save
      logger.debug { "The post was saved, redirecting ..." }
      redirect_to @post, notice: 'Your post was created.'
    else
      logger.debug { "Post invalid: #{@post.errors.full_messages.inspect}" }
      render :new, status: :unprocessable_content
    end
  end

  # ...
end
Started POST "/posts" for 127.0.0.1 at 2026-03-30 18:19:42 +0100
Processing by PostsController#create as HTML
  Parameters: {"post" => {"title" => "Debugging Rails", "body" => "Debugging using application logs..."}}
New post instantiated: {"id" => nil, "title" => "Debugging Rails", "body" => "Debugging using application logs...", "created_at" => nil, "updated_at" => nil}
  TRANSACTION (0.2ms)  BEGIN immediate TRANSACTION /*action='create',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/controllers/posts_controller.rb:12:in 'PostsController#create'
  Post Create (2.0ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ('Debugging Rails', 'Debugging using application logs...', '2026-03-30 17:19:42.522214', '2026-03-30 17:19:42.522214') RETURNING "id" /*action='create',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/controllers/posts_controller.rb:12:in 'PostsController#create'
  TRANSACTION (0.2ms)  COMMIT TRANSACTION /*action='create',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/controllers/posts_controller.rb:12:in 'PostsController#create'
The post was saved, redirecting ...
Redirected to http://localhost:3000/posts/1
↳ app/controllers/posts_controller.rb:14:in 'PostsController#create'
Completed 302 Found in 10ms (ActiveRecord: 2.4ms (1 query, 0 cached) | GC: 0.0ms)

Adding granular logging like this makes it easier to track down unexpected behavior in your application.

2.3. Log Destinations

Rails can write logs to multiple destinations. This is facilitated using ActiveSupport::BroadcastLogger which wraps one or more loggers (known as broadcasts). When a message is logged, it is propogated to each logger. Rails.logger will return an instance of ActiveSupport::BroadcastLogger.

(dev):001> Rails.logger.class
=> ActiveSupport::BroadcastLogger

The default logger is an instance of ActiveSupport::Logger which includes ActiveSupport::TaggedLogging to enable stamping log messages with tags. This object can be accessed using Rails.logger.broadcasts.first:

(dev):001> Rails.logger.broadcasts.first.class
=> ActiveSupport::Logger

When running the Rails server in development (using bin/rails server), an additional logger which writes to STDOUT is also added. This way, logs are visible in the console, as well as written to the log/development.log file.

Invoking the Rails console (using bin/rails console) also adds a second logger which writes to STDERR. This ensures log output doesn't interfere with the REPL (Read-Eval-Print-Loop) which requires control of STDIN and STDOUT.

These secondary loggers are also instances of ActiveSupport::Logger, but do not include ActiveSupport::TaggedLogging.

2.4. Impact of Logs on Performance

Logging has small impact on the performance of your Rails app, particularly when logging to disk.

The greater the number of strings written to the log, the greater the performance penalty. Use an appropriate log level in production to ensure a balance between performance overhead and quantity of logged information.

Always pass a block to the logger, rather than a string.

# DO
logger.debug { "Post: #{@post.inspect}" }

# DON'T
logger.debug "Post: #{@post.inspect}"

When you pass a string to the logger, Ruby evaluates it regardless of log level. This means the (somewhat heavy) String object has to be instantiated and the variables interpolated even if the message won't be logged based on the current level. Blocks, on the other hand, are lazily evaluated only when the current log level includes the message.

These are micro-optimizations. Performance savings are only noticeable with large amounts of logging, however, it is recommended as a best-practice.

2.5. Tagged Logging

In complex applications, the ability to filter logs based on certain rules is a useful tool. The tagged method can be used to add stamps to your log messages, which can then be used for filtering.

logger.tagged("BCX").info { "Stuff" }
# "[BCX] Stuff"

logger.tagged("BCX", "Jason").info { "Stuff" }
# "[BCX] [Jason] Stuff"

logger.tagged("BCX") { logger.tagged("Jason").info { "Stuff" } }
# "[BCX] [Jason] Stuff"

Tags are only added to log messages written to the log file, or to STDOUT in production. They are not visible in the console.

3. Advanced Logging

There are a few advanced configuration options and techniques you can use to maximize the utility of logs when debugging.

3.1. Verbose Query Logs

The verbose_query_logs option enables logging of the source location from where each SQL query is triggered. This is useful to inspect how many SQL queries a request is triggering, and fix any code triggering excessive queries.

It's enabled by default in development.

# config/environments/development.rb

config.active_record.verbose_query_logs = true

Consider an example where a Post model belongs_to an Author model. The below controller and view would generate the following logs when it is rendered:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end
<h1>Posts</h1>
<ul>
  <%= @posts.each do |post| %>
    <li>
      <hgroup>
        <h2><%= post.title %></h2>
        <p><%= post.author.name %></p>
      </hgroup>
    </li>
  <% end %>
</ul>
Started GET "/posts" for 127.0.0.1 at 2026-03-31 17:01:02 +0100
  ActiveRecord::SchemaMigration Load (0.1ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='RailsGuidesDemo'*/
Processing by PostsController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering posts/index.html.erb within layouts/application
  Post Load (0.5ms)  SELECT "posts".* FROM "posts" /*action='index',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/views/posts/index.html.erb:4
  Author Load (0.0ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = 1 LIMIT 1 /*action='index',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/views/posts/index.html.erb:8
  CACHE Author Load (0.0ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = 1 LIMIT 1
  ↳ app/views/posts/index.html.erb:8
  CACHE Author Load (0.0ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = 1 LIMIT 1
  ↳ app/views/posts/index.html.erb:8
  Rendered posts/index.html.erb within layouts/application (Duration: 8.5ms | GC: 0.0ms)
  Rendered layout layouts/application.html.erb (Duration: 12.9ms | GC: 0.0ms)
Completed 200 OK in 24ms (Views: 13.0ms | ActiveRecord: 1.2ms (4 queries, 2 cached) | GC: 0.1ms)

The highlighted lines above are controlled by the verbose_query_logs option. In this case, it demonstrates an N+1 query and its source file and line. We can fix it by eager loading the Author records: Post.includes(:author).all

Avoid using this setting in production. It invokes Ruby's Kernel#caller method which tends to allocate a lot of memory in order to generate stacktraces of method calls. Use SQL Query Logs instead.

3.2. SQL Query Logs

SQL statements can be decorated with a comment containing tags with runtime information, such as the name of the controller or job that triggered the query. This can aid with tracing troublesome queries back to their source.

This option is enabled by default in development.

# config/environments/development.rb

config.active_record.query_log_tags_enabled = true

Enabling Query tags automatically disables prepared statements by default, because each query has a high chance of being unique, making prepared statements a useless overhead.

Rails logs the name of the application, along with the name and action of the controller or the name of the job. It is formatted for SQLCommenter.

Post Load (0.5ms)  SELECT "posts".* FROM "posts" /*action='index',application='RailsGuidesDemo',controller='posts'*/

The log can be customized using config.active_record.query_log_tags. Consult the API documentation for details.

3.3. Verbose Enqueue Logs

verbose_enqueue_logs is a configuration option for Active Job which logs the source location of the line which enqueues a job. This option is enabled by default in development.

# config/environments/development.rb
config.active_job.verbose_enqueue_logs = true

Consider a Post model which has a notify method that enqueues a SendNotificationJob. The highlighed line below will only be logged with verbose_enqueue_logs enabled:

(dev):001> Post.first.notify
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*application='RailsGuidesDemo'*/
Enqueued SendNotificationJob (Job ID: 586340a5-5186-40ee-b5a3-21d173dced70) to Async(default) with arguments: {source: #<GlobalID:0x000000012ee5e960 @uri=#<URI::GID gid://rails-guides-demo/Post/1>>}
↳ app/models/post.rb:6:in 'Post#notify'

Avoid using this setting in production. It invokes Ruby's Kernel#caller method which tends to allocate a lot of memory in order to generate stacktraces of method calls.

3.4. Verbose Redirect Logs

Using the verbose_redirect_logs configuration option, you can control whether the source location of the redirect_to invocation is logged when a request is redirected. It's enabled by default in development.

# config/environments/development.rb
config.action_dispatch.verbose_redirect_logs = true

The highlighted line below is only logged when this option is enabled.

Redirected to http://localhost:3000/posts/1
↳ app/controllers/posts_controller.rb:32:in `block (2 levels) in create'

Avoid using this setting in production. It invokes Ruby's Kernel#caller method which tends to allocate a lot of memory in order to generate stacktraces of method calls.

3.5. Custom Loggers

The logger can be customized to use another utility such as Log4r:

# config/environments/production.rb

config.logger = Log4r::Logger.new("Application Log")

4. Using the debug Gem

A debugger is used to pause program execution, and then manually control further execution while running commands to inspect the application's state. Ruby's native debugger is provided by the debug gem, and is installed in Rails apps by default. The gem is not available in the production environment.

debug is excluded when using alternate Ruby implementations such as JRuby or TruffleRuby. It's only included when running CRuby (sometimes known as MRI).

This section contains a brief overview of key debugger features. Consult the documentation for detailed usage information.

4.1. Entering the Debugger

Add a breakpoint in your code using debugger or one of its aliases: binding.break and binding.b.

class PostsController < ApplicationController
  def index
    @posts = Post.all
    debugger
  end
  # ...
end

When the app evaluates the debugger statement, execution will pause, and you can access the debugger in your console:

Processing by PostsController#index as HTML
[1, 6] in ~/projects/rails-guides-demo/app/controllers/posts_controller.rb
     1| class PostsController < ApplicationController
     2|   def index
     3|     @posts = Post.all
=>   4|     debugger
     5|   end
     6| end
=>#0  PostsController#index at ~/projects/rails-guides-demo/app/controllers/posts_controller.rb:4
  #1  ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/action_controller/metal/basic_implicit_render.rb:8
  # and 78 frames (use `bt' command for all frames)
(rdbg)

Within this prompt, you can run debugger commands to inspect the state of your application. Continue program execution using continue (or c), or quit the debugger and terminate your program using quit (or q).

When inside the debugger, you can run Ruby code just like IRB or the Rails console.

(ruby) @posts
  Post Load (1.7ms)  SELECT "posts".* FROM "posts" /* loading for pp */ LIMIT 11 /*action='index',application='RailsGuidesDemo',controller='posts'*/
  ↳ app/controllers/posts_controller.rb:4:in 'PostsController#index'
[#<Post:0x00000001225d2be0 id: 1 ...>,
 #<Post:0x00000001225d2aa0 id: 2 ...>]
(rdbg) self
#<PostsController:0x00000000006330>
(rdbg)

4.2. Debugger Commands

Besides direct evaluation, the debugger supports a plethora of commands to collect information and control program flow. Some examples are:

  • help (or h) - Show help for all commands.
  • step (or s) - Step into the current line.
  • next (or n) - Resume program until the next line.
  • backtrace (or bt) - Show the backtrace with additional related information.
  • break (or b) - List or modify breakpoints.

When a Ruby expression clashes with a debugger command, evaluate it using p or eval. For example, if you had a variable called step, you'd evaluate it by running: p step. You can also use pp which will pretty print the output.

4.2.1. Control Flow

When inside the debugger, you can control program execution. For example, you can continue execution for a set number of lines before the program pauses once again with the interactive debugger prompt.

Some useful commands are:

# Step into the current line
(rdbg) step

# Continue to the next line
(rdbg) next

# Continue for 5 lines
(rdbg) next 5

The debug gem documentation lists all available control flow commands.

4.2.2. backtrace

The backtrace command lists all the frames on the stack:

=>#0  PostsController#index at ~/projects/rails-guides-demo/app/controllers/posts_controller.rb:4
  #1  ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/action_controller/metal/basic_implicit_render.rb:8
  #2  AbstractController::Base#process_action at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/abstract_controller/base.rb:221
  #3  ActionController::Rendering#process_action at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/action_controller/metal/rendering.rb:199
  #4  block in AbstractController::Callbacks#process_action at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/abstract_controller/callbacks.rb:267
  #5  block in ActiveSupport::Callbacks#run_callbacks at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/activesupport-8.1.2/lib/active_support/callbacks.rb:121
  #6  Turbo.with_request_id(request_id=nil) at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/turbo-rails-2.0.23/lib/turbo-rails.rb:24
  ..... and more

Every frame includes:

  • The frame identifier
  • Call location
  • Additional information (such as block or method arguments)

While this is useful, there are far too many frames and they're mostly from within Rails and other libraries. You can specify how many frames are printed out by supplying a number to backtrace:

(rdbg) backtrace 11

A regex can be used to filter frames by its identifier or location:

(rdbg) backtrace /Posts/
(rdbg) backtrace /actionpack/

These options can also be used together:

(rdbg) backtrace 11 /actionpack/

4.2.3. break

Dynamically add and remove breakpoints while inside the debugger using break (or b).

# List all breakpoints
(rdbg) break

# Set a breakpoint on a specific line of the current file
(rdbg) break 7

# Set a breakpoint on a line in another file
(rdbg) break app/controllers/books_controller.rb:12

# Set a breakpoint on a method in specific class
(rdbg) break BooksController#index

When setting a breakpoint in another file, you need to specify its relative path from your project's root. In the development environment, constants are lazily auto-loaded when referenced. Hence, a breakpoint in another file that hasn't been loaded yet will show as (pending) until that file is referenced and automatically loaded, at which point the breakpoint will become active.

Remove breakpoints using delete (or del):

# Delete all breakpoints
(rdbg) del

# Delete the breakpoint using its ID
(rdbg) del 21

4.2.4. catch

Add a breakpoint where an exception is raised.

(rdbg) catch ActiveRecord::RecordInvalid

4.2.5. watch

Add a breakpoint where an instance variable is mutated.

(rdbg) watch @posts

The debugger supports several more commands, and more options even within the commands demonstrated. Consult the documentation for a complete manual.

4.3. Program Your Debugging Workflow

The debugger statement supports do: and pre: keywords to add automation to your debugging workflow.

do: pauses the program, runs the supplied value as a debugging command, and then continues the program. Use this when you want to run a specific debugger command without stopping the program.

The below example runs info and adds a breakpoint when @posts is modified, then continues the program.

class PostsController < ApplicationController
  def index
    @posts = Post.all
    debugger(do: "info \n watch @posts")
  end
end

pre: runs the supplied debugging command and keeps the program suspended so you can use the interactive console. This is useful for automatically printing relevant information such as the backtrace when the breakpoint is hit.

class PostsController < ApplicationController
  def index
    @posts = Post.all
    debugger(pre: "backtrace 10")
  end
end

4.4. Remote Debugging

A Rails server in development might be run alongside other programs such as a jobs executor, or a CSS/JavaScript watcher (provided by cssbundling-rails or jsbundling-rails). This is usually done using foreman via a Procfile invoked by running bin/dev.

Debugging a program requires it to be running within a terminal. When foreman is used to run multiple programs, it forks them as child processes and pipes their input and output into the parent process. As such the Rails server process isn't running within a terminal and cannot be used for interactive debugging.

To remedy this, the auto-generated Procfile.dev sets the environment variable RUBY_DEBUG_OPEN=true when starting the Rails server to enable remote debugging.

$ bin/dev

14:11:34 web.1  | DEBUGGER: Debugger can attach via UNIX domain socket (/var/folders/z2/z6q9dxmd4mb9v_8khdtgsll00000gn/T/rdbg-501/rdbg-24515)
14:11:34 web.1  | DEBUGGER: wait for debugger connection...

The debugger now needs to be externally attached to the server process, rather than run within it. Run rdbg -a in a new terminal window to attach the debugger to your Rails process and start a session.

$ rdbg -a
DEBUGGER (client): Connected. PID:24515, $0:bin/rails

[1, 10] in ~/projects/rails-guides-demo/app/controllers/posts_controller.rb
     1| class PostsController < ApplicationController
     2|   def index
     3|     @posts = Post.all
=>   4|     debugger
     5|   end
     6| end
=>#0  PostsController#index at ~/projects/rails-guides-demo/app/controllers/posts_controller.rb:4
  #1  ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/actionpack-8.1.2/lib/action_controller/metal/basic_implicit_render.rb:8
  # and 78 frames (use `bt' command for all frames)
(rdbg:remote)

5. Debugging in the View

Sometimes it's useful to dump debug output right into the view, so it can be inspected in the browser.

5.1. The debug helper

The debug helper method renders a <pre> tag containing a YAML dump of the supplied object.

For example, the below view code:

<%= debug @post %>

<h1><%= @post.title %></h1>

will render the following HTML:

<pre class="debug_dump">
  --- !ruby/object:Post
  concise_attributes:
  - !ruby/object:ActiveModel::Attribute::FromDatabase
    name: id
    value_before_type_cast: 1
  - !ruby/object:ActiveModel::Attribute::FromDatabase
    name: title
    value_before_type_cast: Debugging Rails with View Helpers
  - !ruby/object:ActiveModel::Attribute::FromDatabase
    name: body
    value_before_type_cast: "...";
  - !ruby/object:ActiveModel::Attribute::FromDatabase
    name: created_at
    value_before_type_cast: "2026-03-30 15:04:27.776801";
  - !ruby/object:ActiveModel::Attribute::FromDatabase
    name: updated_at
    value_before_type_cast: "2026-03-30 15:04:27.776801";
  new_record: false
  active_record_yaml_version: 2
</pre>

<h1>Debugging Rails with View Helpers</h1>

5.2. inspecting Objects

The inspect method is available on all Ruby objects except BasicObject. It provides a human-readable representation of the object, and hence can be useful to render in a view for debugging.

<%= @post.inspect %>

<h1><%= @post.title></h1>

renders:

#<Post id: 1, title: "Debugging Rails with View Helpers", body: "...";, created_at: "2026-03-30 15:04:27.776801000 +0000", updated_at: "2026-03-30 15:04:27.776801000 +0000">

<h1>Debugging Rails with View Helpers</h1>

The inspect method can be overridden just like any other method. As such, its output may differ significantly depending on the object.

5.3. Web-based Ruby Console

The web-console gem allows you to inject an interactive Ruby console within your view. It's installed in Rails apps for the development environment.

Add a console directive in your controller or view.

class PostsController < ApplicationController
  def index
    @posts = Post.all
    console
  end
end

or

<% console %>

<h1>Posts</h1>

Navigate to /posts in your browser and you'll see a console at the bottom of the screen. This is an interative prompt where you can evaluate Ruby expressions to inspect the current state of your application.

Only one console can be rendered per request. web-console will raise an error on a second console invocation.

See the gem's Readme for advanced usage and configuration options.

Since web-console evaluates plain Ruby code remotely on the server, never use it in production.

6. Debugging Memory Leaks

All Ruby applications can leak memory — either within the Ruby itself or lower down in C code. This is a broad topic so this guide will not go into detail on this subject.

A general approach for finding Ruby memory leaks is to use rbtrace, to extract a heap dump and analyze it with a heap analyzer.



Back to top