In Part 1, we learned how to spot bottlenecks using Rails logs. Now let’s go deeper โ using profiling tools, custom logging, and real-world fixes (including our Flipper case).
๐งฐ Profiling Tools for Rails Developers
1. Rack Mini Profiler ๐
Rack Mini Profiler is the go-to gem for spotting slow DB queries and views.
Add it to your Gemfile (development & staging only):
group :development do
gem 'rack-mini-profiler'
end
Then run:
bundle install
When you load a page, youโll see a little timing panel in the top-left corner:
Total time per request.
SQL queries count & time.
Which queries are repeated.
View rendering breakdown.
This makes N+1 queries immediately visible.
2. Bullet ๐ซ (for N+1 Queries)
Add to Gemfile:
group :development do
gem 'bullet'
end
Config in config/environments/development.rb:
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
Now, if you forget to eager load (includes), Bullet will warn:
N+1 query detected: Review => Product
Add to your query: .includes(:product)
3. Custom Logging for SQL Queries ๐
Sometimes you need to trace where in your code a slow query is triggered. Rails lets you hook into ActiveRecord logging:
# config/initializers/query_tracer.rb
ActiveSupport::Notifications.subscribe("sql.active_record") do |_, start, finish, _, payload|
duration = (finish - start) * 1000
if duration > 100 # log queries > 100ms
Rails.logger.warn "SLOW QUERY (#{duration.round(1)}ms): #{payload[:sql]}"
Rails.logger.warn caller.select { |line| line.include?(Rails.root.to_s) }.first(5).join("\n")
end
end
This logs:
The query.
Execution time.
Stack trace (where in Rails code it was triggered).
โก Real Example: Fixing Flipper Performance
In Part 1, we saw Flipper queries running 38 times per page:
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"
Problem ๐ฅ
Each Flipper.enabled?(:feature_name, user) call hit the DB. With dozens of flags per request โ repeated queries โ 6s page loads.
Solution โ : Redis Caching
Flipper supports caching with Redis.
# config/initializers/flipper.rb
require 'flipper/adapters/active_record'
require 'flipper/adapters/redis'
require 'flipper/adapters/operation_logger'
flipper_db_adapter = Flipper::Adapters::ActiveRecord.new
flipper_redis_adapter = Flipper::Adapters::Redis.new(Redis.new)
# wrap with memory cache to avoid repeat queries
flipper_caching_adapter = Flipper::Adapters::Cache.new(
flipper_db_adapter,
cache: flipper_redis_adapter,
expires_in: 5.minutes
)
Flipper.configure do |config|
config.default = Flipper.new(flipper_caching_adapter)
end
Now:
First request โ fetches from DB, writes to Redis.
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.
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
Reproduce the slow page locally or in staging.
Tail the logs and isolate the slow request.
Categorize: rendering slow? DB queries slow? external API calls?
Identify repeated or long queries.
Ask “why“:
Missing index?
Bad join?
N+1 query?
Repeated lookups that could be cached?
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).
Ruby on Rails has always been about developer happiness and productivity. With Rails 7.2, the framework took a significant step forward by including RuboCop as a built-in tool for new applications. This feature continues in Rails 8.0 and represents a major shift in how Rails approaches code quality and consistency.
RuboCop is a powerful static code analyzer, linter, and code formatter for Ruby. It enforces coding standards based on the community Ruby Style Guide and helps developers:
Maintain consistent code style across projects and teams
Identify potential bugs and code smells early
Automatically fix many style violations
Improve code readability and maintainability
Think of RuboCop as your personal code reviewer that never gets tired and always applies the same standards consistently.
๐ What This Means for Rails Developers
The inclusion of RuboCop as a default tool in Rails represents several significant changes:
๐ฏ Standardization Across the Ecosystem
Consistent code style across Rails applications
Reduced onboarding time for new team members
Easier code reviews with automated style checking
๐ Improved Developer Experience
No more manual setup for basic linting
Immediate feedback on code quality
Built-in best practices from day one
๐ Educational Benefits
Learning tool for new Ruby developers
Enforcement of Ruby community standards
Gradual improvement of coding skills
โฐ Before Rails 7.2: The Manual Setup Era
๐ง Manual Installation Process
Before Rails 7.2, integrating RuboCop required several manual steps:
# Omakase Ruby styling for Rails
inherit_gem:
rubocop-rails-omakase: rubocop.yml
# Your own specialized rules go here
๐ Before vs After: Key Differences
Aspect
Before Rails 7.2
After Rails 7.2
๐ง Setup
Manual, time-consuming
Automatic, zero-config
๐ Consistency
Varies by project/team
Standardized omakase style
โฑ๏ธ Time to Start
15-30 minutes setup
Immediate
๐ฏ Configuration
Custom, often overwhelming
Minimal, opinionated
๐ Learning Curve
Steep for beginners
Gentle, guided
๐ Maintenance
Manual updates needed
Managed by Rails team
โก Advantages of Built-in RuboCop
๐ฅ For Development Teams
๐ฏ Immediate Consistency
No configuration debates – omakase style provides sensible defaults
Faster onboarding for new team members
Consistent code reviews across all projects
๐ Increased Productivity
Less time spent on style discussions
More focus on business logic
Automated code formatting saves manual effort
๐ซ For Learning and Education
๐ Built-in Best Practices
Ruby community standards enforced by default
Immediate feedback on code quality
Educational comments in RuboCop output
๐ Skill Development
Gradual learning of Ruby idioms
Understanding of performance implications
Code smell detection capabilities
๐ข For Organizations
๐ Code Quality
Consistent standards across all Rails projects
Reduced technical debt accumulation
Easier maintenance of legacy code
๐ฐ Cost Benefits
Reduced code review time
Fewer bugs in production
Faster developer onboarding
๐ ๏ธ Working with RuboCop in Rails 7.2+
๐ Getting Started
1. ๐โโ๏ธ Running RuboCop
# Check your code
./bin/rubocop
# Auto-fix issues
./bin/rubocop -a
# Check specific files
./bin/rubocop app/models/user.rb
# Check with different format
./bin/rubocop --format json
2. ๐ Understanding Output
$ ./bin/rubocop
Inspecting 23 files
.......C..............
Offenses:
app/models/user.rb:15:81: C: Layout/LineLength: Line is too long. [95/80]
def full_name; "#{first_name} #{last_name}"; end
1 file inspected, 1 offense detected, 1 offense autocorrectable
โ๏ธ Customizing Configuration
๐จ Adding Your Own Rules
Edit .rubocop.yml to add project-specific rules:
# Omakase Ruby styling for Rails
inherit_gem:
rubocop-rails-omakase: rubocop.yml
# Your own specialized rules go here
Metrics/LineLength:
Max: 120
Style/Documentation:
Enabled: false
# Exclude specific files
AllCops:
Exclude:
- 'db/migrate/*'
- 'config/routes.rb'
๐ง Common Customizations
# Allow longer lines in specs
Metrics/LineLength:
Exclude:
- 'spec/**/*'
# Disable specific cops for legacy code
Style/FrozenStringLiteralComment:
Exclude:
- 'app/legacy/**/*'
# Custom naming patterns
Naming/FileName:
Exclude:
- 'lib/tasks/*.rake'
# Begin with defaults
inherit_gem:
rubocop-rails-omakase: rubocop.yml
# Add team-specific rules only when needed
Metrics/ClassLength:
Max: 150 # Team prefers slightly longer classes
2. ๐ Use Auto-correction Wisely
# Safe auto-corrections
./bin/rubocop -a
# All auto-corrections (review changes!)
./bin/rubocop -A
# Check what would be auto-corrected
./bin/rubocop --auto-correct --dry-run
3. ๐ Gradual Legacy Code Improvement
# Use rubocop_todo.yml for existing code
inherit_from:
- .rubocop_todo.yml
# Generate todo file for legacy code
# $ bundle exec rubocop --auto-gen-config
๐ก๏ธ Handling Violations
๐ฏ Prioritizing Fixes
๐ด High Priority: Security and bug-prone patterns
๐ก Medium Priority: Performance issues
๐ข Low Priority: Style preferences
๐ Selective Disabling
# Disable for specific lines
user_data = some_complex_hash # rubocop:disable Metrics/LineLength
# Disable for blocks
# rubocop:disable Metrics/AbcSize
def complex_method
# Complex but necessary logic
end
# rubocop:enable Metrics/AbcSize
Better AI-generated code following Rails conventions
Consistent output from coding assistants
Improved code suggestions in IDEs
๐ข Industry Impact
๐ Hiring and Onboarding
Faster developer onboarding with consistent standards
Easier code assessment during interviews
Reduced training time for Rails conventions
๐ Code Review Process
Automated style checking reduces manual review time
Focus on logic rather than formatting
Consistent feedback across different reviewers
๐ Advanced Usage Patterns
๐ฏ Team-Specific Configurations
# .rubocop.yml for different team preferences
inherit_gem:
rubocop-rails-omakase: rubocop.yml
# Backend team preferences
Metrics/MethodLength:
Max: 15
# Frontend team (dealing with complex views)
Metrics/AbcSize:
Exclude:
- 'app/helpers/**/*'
# QA team (longer test descriptions)
Metrics/LineLength:
Exclude:
- 'spec/**/*'
# Generate detailed reports
./bin/rubocop --format json --out rubocop.json
./bin/rubocop --format html --out rubocop.html
# Focus on specific cop families
./bin/rubocop --only Layout
./bin/rubocop --only Security
./bin/rubocop --only Performance
๐ Conclusion
The inclusion of RuboCop as a built-in tool in Rails 8.0 (starting from 7.2) represents a significant evolution in the Rails ecosystem. This change brings numerous benefits:
๐ Consistent code quality across the Rails community
๐ Educational benefits for developers at all levels
โก Improved productivity through automation
๐จ Balanced approach between opinionated defaults and flexibility
๐ฎ Looking Forward
As the Rails community adapts to this change, we can expect:
Better code consistency across open-source Rails projects
Improved developer experience for newcomers
Enhanced tooling integration throughout the ecosystem
Continued evolution of the omakase philosophy
๐ก Final Recommendations
๐ฏ Embrace the defaults initially – they’re well-considered
๐ Learn from violations rather than just fixing them
๐ Customize gradually based on team needs
๐ค Use it as a teaching tool for junior developers
๐ Monitor improvements in code quality over time
The built-in RuboCop integration exemplifies Rails’ commitment to developer happiness and productivity. By providing sensible defaults while maintaining flexibility, Rails continues to evolve as a framework that scales with teams and projects of all sizes.
Whether you’re starting a new Rails project or maintaining an existing one, RuboCop’s integration offers an opportunity to improve code quality and developer experience with minimal effort. Embrace the omakase philosophy, customize where needed, and enjoy cleaner, more consistent Ruby code! ๐
Have you started using RuboCop with Rails 8.0? Share your experiences and customizations in the comments below!