Detect N+1 queries in Rails applications with zero configuration and actionable fix suggestions.
AndOne stays completely invisible until it detects an N+1 query — then it tells you exactly what's wrong and how to fix it. No external dependencies beyond Rails itself.
- Zero configuration — Railtie auto-setup in development and test
- Actionable fix suggestions — suggests the exact
.includes(),.preload(), or.eager_load()call - Smart location detection — identifies both the origin (where the N+1 fires) and the fix location (where to add
.includes) - Clean error handling — never corrupts backtraces or interferes with exception propagation
- No external dependencies — only Rails itself
- Auto-raises in test — N+1s fail your test suite by default
- Background job support — ActiveJob (
around_perform) and Sidekiq server middleware, with double-scan protection - Ignore file —
.and_one_ignorewithgem:,path:,query:, andfingerprint:rules - Automatic deduplication — each unique N+1 is reported once per server session with occurrence counts
- Test matchers — Minitest (
assert_no_n_plus_one) and RSpec (expect { }.not_to cause_n_plus_one) - Dev toast notifications — in-page toast on every page that triggers an N+1, with a link to the dashboard
- Dev UI dashboard — browse
/__and_onein development for a live N+1 overview - Rails console integration — auto-scans in
rails consoleand prints warnings inline - Structured JSON logging — JSON output mode for Datadog, Splunk, and other log aggregation services
- Per-environment thresholds — different
min_n_queriesfor development vs test - GitHub Actions annotations — N+1s appear as warning annotations on PR diffs
strict_loadingsuggestions — also suggests model-level prevention as an alternativehas_many :throughand polymorphic support — resolves complex association chains- Thread-safe under Puma — per-thread isolation verified with concurrent stress tests
Add to your Gemfile:
group :development, :test do
gem "and_one"
endThat's it. AndOne automatically activates in development and test environments via a Railtie.
When an N+1 is detected, you get output like:
──────────────────────────────────────────────────────────────────────────
🏀 And One! 1 N+1 query detected
──────────────────────────────────────────────────────────────────────────
1) 9x repeated query on `comments`
fingerprint: a1b2c3d4e5f6
Query:
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Origin (where the N+1 is triggered):
→ app/views/posts/index.html.erb:5
Fix here (where to add .includes):
⇒ app/controllers/posts_controller.rb:8
Call stack:
app/views/posts/index.html.erb:5
app/controllers/posts_controller.rb:8
💡 Suggestion:
Add `.includes(:comments)` to your Post query
To ignore, add to .and_one_ignore:
fingerprint:a1b2c3d4e5f6
──────────────────────────────────────────────────────────────────────────
Automatically hooked via around_perform. Works with every ActiveJob backend:
Sidekiq, GoodJob, SolidQueue, Delayed Job, Resque, and anything else that uses ActiveJob.
No configuration needed — the Railtie handles it.
For jobs that use Sidekiq directly (bypassing ActiveJob), AndOne installs a server middleware automatically when Sidekiq is detected.
If you need manual installation:
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add AndOne::SidekiqMiddleware
end
endWhen both hooks are active (ActiveJob job running through Sidekiq), the Sidekiq middleware detects the existing scan from ActiveJobHook and passes through — no double-scanning.
Create a .and_one_ignore file in your project root to permanently silence known N+1s. Supports four rule types:
# Ignore N+1s originating from a specific gem
# (matches against raw backtrace paths, e.g. /gems/devise-4.9.0/)
gem:devise
gem:administrate
# Ignore N+1s whose call stack matches a path pattern (supports * globs)
path:app/views/admin/*
path:lib/legacy/**
# Ignore N+1s matching a SQL pattern
query:schema_migrations
query:pg_catalog
# Ignore a specific detection by its fingerprint (shown in output)
fingerprint:a1b2c3d4e5f6This is especially useful for N+1s coming from gems where you can't add .includes() to the source. Instead of littering your code with AndOne.pause blocks, add a gem: rule.
| Rule | Use when... |
|---|---|
gem:devise |
A gem you depend on has an N+1 you can't fix |
path:app/views/admin/* |
An area of your app has known N+1s you've accepted |
query:some_table |
A specific query pattern should always be ignored |
fingerprint:abc123 |
You want to silence one specific detection (shown in output) |
In development, the same N+1 can fire on every request, flooding your logs. AndOne automatically deduplicates — each unique pattern is reported only once per server session. Subsequent occurrences are silently counted.
You can check the session summary at any time:
AndOne.aggregate.summary # formatted string of all unique N+1s
AndOne.aggregate.size # number of unique patterns
AndOne.aggregate.reset! # clear and start freshWhen an N+1 is detected during a request, AndOne injects a small toast notification into the bottom-right corner of the page. The toast shows which tables were affected and links to the full dashboard for details.
This is enabled by default in development — no configuration needed. The toast auto-dismisses after 8 seconds, but hovering over it keeps it open.
To change the position or disable it:
# config/initializers/and_one.rb
AndOne.dev_toast_position = :bottom_right # :top_right (default), :top_left, :bottom_right, :bottom_left
AndOne.dev_toast = false # disable entirelyThe toast only appears on HTML responses with a 200 status, so it won't interfere with API endpoints, redirects, or error pages.
Browse /__and_one in development for a full overview of every unique N+1 detected in the current server session. The dashboard shows the query, origin, fix location, and suggested .includes() call for each detection.
Both features work together: the toast gives you immediate feedback on the page you're looking at, and the dashboard link takes you to the full picture.
class PostsControllerTest < ActionDispatch::IntegrationTest
include AndOne::MinitestHelper
test "index does not cause N+1 queries" do
assert_no_n_plus_one do
get posts_path
end
end
test "known N+1 is documented" do
detections = assert_n_plus_one do
get legacy_report_path
end
assert_equal "comments", detections.first.table_name
end
end# In spec_helper.rb or rails_helper.rb
require "and_one/rspec"
# Then in your specs
RSpec.describe "Posts" do
it "loads posts efficiently" do
expect {
Post.includes(:comments).each { |p| p.comments.to_a }
}.not_to cause_n_plus_one
end
it "has a known N+1" do
expect {
Post.all.each { |p| p.comments.to_a }
}.to cause_n_plus_one
end
endThe matchers temporarily disable raise_on_detect internally, so they work correctly regardless of your global configuration.
- Development: Logs N+1 warnings to Rails logger and stderr
- Test: Raises
AndOne::NPlus1Errorso N+1s fail your test suite - Production: Completely disabled (not even loaded)
AndOne works out of the box, but you can customize:
# config/initializers/and_one.rb
AndOne.configure do |config|
# Raise on detection (default: true in test, false in development)
config.raise_on_detect = false
# Minimum repeated queries to trigger (default: 2)
config.min_n_queries = 3
# In-page toast notifications (default: true in development)
config.dev_toast = true
# Toast position (default: :top_right)
# Options: :top_right, :top_left, :bottom_right, :bottom_left
config.dev_toast_position = :top_right
# Path to ignore file (default: Rails.root/.and_one_ignore)
config.ignore_file_path = Rails.root.join(".and_one_ignore").to_s
# Allow specific patterns (won't flag these call stacks)
config.allow_stack_paths = [
/admin_controller/,
/some_legacy_code/
]
# Ignore specific query patterns
config.ignore_queries = [
/pg_catalog/,
/schema_migrations/
]
# Custom backtrace cleaner
config.backtrace_cleaner = Rails.backtrace_cleaner
# Custom callback for integrations (logging services, etc.)
config.notifications_callback = ->(detections, message) {
# detections is an array of AndOne::Detection objects
# message is the formatted string
MyLogger.warn(message)
}
endYou can also scan specific blocks:
# In a test
detections = AndOne.scan do
posts = Post.all
posts.each { |p| p.comments.to_a }
end
assert_empty detections
# Pause/resume within a scan
AndOne.scan do
# This is scanned
posts.each { |p| p.comments.to_a }
AndOne.pause do
# This is NOT scanned
legacy_code_with_known_n_plus_ones
end
# Scanning resumes automatically after the pause block
end- Subscribe to
sql.active_recordnotifications (built into Rails) - Group queries by call stack fingerprint
- Fingerprint SQL to detect same-shape queries with different bind values
- Resolve table names back to ActiveRecord models and associations
- Suggest the exact
.includes()call to fix the N+1 - Filter against the
.and_one_ignorefile and aggregate tracker
The middleware is designed to never interfere with error propagation. If your app raises an exception during a request, AndOne silently stops scanning and re-raises the original exception with its backtrace completely intact.
The gem is available as open source under the terms of the MIT License.