๐Ÿšฆ Classic Performance Debugging Problems in Rails Apps โ€” Part 1: Finding the Bottlenecks

Rails makes building apps fast and joyful โ€” but sooner or later, every team runs into the same dreaded complaint:

“Why is this page so slow?”

Performance debugging is tricky because Rails abstracts so much for us. Underneath every User.where(...).first or current_user.orders.includes(:products), there’s real SQL, database indexes, network calls, caching layers, and Ruby code running.

This post (Part 1) focuses on how to find the bottlenecks in a Rails app using logs and manual inspection. In Part 2, we’ll explore tools like Rack Mini Profiler and real-world fixes.


๐Ÿ”Ž Symptoms of a Slow Rails Page

Before diving into logs, it’s important to recognize what “slow” might mean:

  • Page loads take several seconds.
  • CPU usage spikes during requests.
  • The database log shows queries running longer than expected.
  • Repeated queries (e.g. the same SELECT firing 30 times).
  • Memory bloat or high GC (garbage collection) activity.

Example symptom we hit:

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"

This query was executed 38 times when loading a product page (/product/adidas-shoe). Thatโ€™s a red flag ๐Ÿšฉ.


๐Ÿ“œ Understanding Rails Logs

Every Rails request is logged in log/development.log (or production.log). A typical request looks like:

Started GET "/products/123" for 127.0.0.1 at 2025-09-25 12:45:01 +0530
Processing by ProductsController#show as HTML
  Parameters: {"id"=>"123"}
  Product Load (1.2ms)  SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT $2  [["id", 123], ["LIMIT", 1]]
  Review Load (10.4ms)  SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = $1  [["product_id", 123]]
Completed 200 OK in 120ms (Views: 80.0ms | ActiveRecord: 20.0ms | Allocations: 3456)

Key things to notice:

  • Controller action โ†’ ProductsController#show.
  • Individual SQL timings โ†’ each query shows how long it took.
  • Overall time โ†’ Completed 200 OK in 120ms.
  • Breakdown โ†’ Views: 80.0ms | ActiveRecord: 20.0ms.

If the DB time is small but Views are big โ†’ it’s a rendering problem.
If ActiveRecord dominates โ†’ the DB queries are the bottleneck.


๐Ÿ•ต๏ธ Debugging a Slow Page Step by Step

1. Watch your logs in real time

tail -f log/development.log | grep -i "SELECT"

This shows you every SQL query as it executes.

2. Look for repeated queries (N+1)

If you see the same SELECT firing dozens of times:

SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 123
SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 124
SELECT "reviews".* FROM "reviews" WHERE "reviews"."product_id" = 125

That’s the classic N+1 query problem.

3. Look for expensive joins

Queries with multiple JOINs can be slow without proper indexing. Example:

SELECT "orders"."id", "users"."email"
FROM "orders"
INNER JOIN "users" ON "users"."id" = "orders"."user_id"
WHERE "users"."status" = 'active'

If there’s no index on users.status, this can cause sequential scans.

4. Look for long-running queries

Rails logs include timings:

User Load (105.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 123

If a query consistently takes >100ms on small tables, it probably needs an index or query rewrite.


โšก Real Example: Debugging the Flipper Feature Flag Queries

In our case, the Rails logs showed:

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"

  • It executed 38 times on one page.
  • Each execution took between 60โ€“200ms.
  • Together, that added ~6 seconds to page load time.

The query itself wasn’t huge (tables had <150 rows). The problem was repetition โ€” every feature flag check was hitting the DB fresh.

This pointed us toward caching (covered in Part 2).

๐Ÿงฉ Workflow for Performance Debugging in Rails

  1. Reproduce the slow page locally or in staging.
  2. Tail the logs and isolate the slow request.
  3. Categorize: rendering slow? DB queries slow? external API calls?
  4. Identify repeated or long queries.
  5. Ask “why“:
    • Missing index?
    • Bad join?
    • N+1 query?
    • Repeated lookups that could be cached?
  6. Confirm with SQL tools (EXPLAIN ANALYZE in Postgres).

โœ… Summary of Part 1

In this first part, we covered:

  • Recognizing symptoms of slow pages.
  • Reading Rails logs effectively.
  • Debugging step by step with queries and timings.
  • A real-world case of repeated Flipper queries slowing down a page.

In Part 2, we’ll go deeper into tools and solutions:

  • Setting up Rack Mini Profiler.
  • Capturing queries + stack traces in custom logs.
  • Applying fixes: indexes, eager loading, and caching (with Flipper as a worked example).

Can We Do Type Checking in Ruby Method Parameters?

Ruby is a dynamically typed language that favors duck typing over strict type enforcement. However, there are cases where type checking can be useful to avoid unexpected behavior. In this post, weโ€™ll explore various ways to perform type validation and type checking in Ruby.

Type Checking and Type Casting in Ruby

Yes, even though Ruby does not enforce types at the language level, there are several techniques to validate the types of method parameters. Below are some approaches:

1. Manual Type Checking with raise

One straightforward way to enforce type checks is by manually verifying the type of a parameter using is_a? and raising an error if it does not match the expected type.

def my_method(arg)
  raise TypeError, "Expected String, got #{arg.class}" unless arg.is_a?(String)
  
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(123)      # Raises: TypeError: Expected String, got Integer

2. Using respond_to? for Duck Typing

Rather than enforcing a strict class type, we can check whether an object responds to a specific method.

def my_method(arg)
  unless arg.respond_to?(:to_str)
    raise TypeError, "Expected a string-like object, got #{arg.class}"
  end
  
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(:symbol)  # Raises TypeError

3. Using Ruby 3’s Type Signatures (RBS)

Ruby 3 introduced RBS and TypeProf for static type checking. You can define types in an .rbs file:

def my_method: (String) -> void

Then, you can use tools like steep, a static type checker for Ruby, to enforce type checking at development time.

How to Use Steep for Type Checking

Steep does not use annotations or perform type inference on its own. Instead, it relies on .rbi files to define type signatures. Hereโ€™s how you can use Steep for type checking:

  1. Define a Ruby Class:
class Calculator
  def initialize(value)
    @value = value
  end
  
  def double
    @value * 2
  end
end

  1. Generate an .rbi File:
steep scaffold calculator.rb > sig/calculator.rbi

This generates an .rbi file, but initially, it will use any for all types. You need to manually edit it to specify proper types.

  1. Modify the .rbi File to Define Types:
class Calculator
  @value: Integer
  def initialize: (Integer) -> void
  def double: () -> Integer
end

  1. Run Steep to Check Types:
steep check

Steep also supports generics and union types, making it a powerful but less intrusive type-checking tool compared to Sorbet.

4. Using Sorbet for Stronger Type Checking

Sorbet is a third-party static type checker that allows you to enforce type constraints at runtime.

require 'sorbet-runtime'

extend T::Sig

sig { params(arg: String).void }
def my_method(arg)
  puts "Valid input: #{arg}"
end

my_method("Hello")  # Works fine
my_method(123)      # Raises error at runtime

References:

Another Approach: Using Rescue for Type Validation

A different way to handle type checking is by using exception handling (rescue) to catch unexpected types and enforce validation.

def process_order(order_items, customer_name, discount_code)
  # Main logic
  ...

rescue => e
  # Type and validation checks
  raise "Expecting an array of items: #{order_items.inspect}" unless order_items.is_a?(Array)
  raise "Order must contain at least one item: #{order_items.inspect}" if order_items.empty?
  raise "Expecting a string for customer name: #{customer_name.inspect}" unless customer_name.is_a?(String)
  raise "Customer name cannot be empty" if customer_name.strip.empty?
  
  raise "Unexpected error in `process_order`: #{e.message}"
end

Summary

  • Use is_a? or respond_to? for runtime type checking.
  • Use Ruby 3โ€™s RBS for static type enforcement.
  • Use Sorbet for stricter type checking at runtime.
  • Use Steep for static type checking with RBS.
  • Exception handling can be used for validating types dynamically.

Additional Considerations

Ruby is a dynamically typed language, and unit tests can often be more effective than type checks in ensuring correctness. Writing tests ensures that method contracts are upheld for expected data.

For Ruby versions prior to 3.0, install the rbs gem separately to define types for classes.

If a method is defined, it will likely be called. If reasonable tests exist, every method will be executed and checked. Therefore, instead of adding excessive type checks, investing time in writing tests can be a better strategy.