# Bullet: Detecting N+1 Queries and Eager Loading Problems in Rails Apps

> Bullet is a Ruby gem that hooks into ActiveRecord and Mongoid query logging to identify N+1 queries, unnecessary eager loading, and counter cache opportunities during development. It reports findings through configurable notification channels so developers can fix performance problems before they reach production.

**flyerhzm/bullet** — help to kill N+1 queries and unused eager loading

- Repository: https://github.com/flyerhzm/bullet
- Stars: 7,340 · Forks: 456
- Language: Ruby
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/flyerhzm-bullet

## The N+1 Query Problem Bullet Is Built to Catch

An N+1 query occurs when an application loads a collection of records with one query and then issues an additional query for each record to fetch an associated model. Loading ten posts and then fetching the author for each post separately produces eleven queries rather than one query with a join or two queries with eager loading. This pattern scales badly: one hundred posts become one hundred and one queries, and the cost grows linearly with the collection size.

Bullet watches the queries your application issues during a request and detects when this pattern occurs. It also detects the opposite case: when you have told ActiveRecord to eagerly load an association with includes() but the association is never actually used in the request, wasting the join. The third detection is for counter cache opportunities, where a COUNT query on a belongs_to association could be replaced by a cached integer column on the parent record.

According to the README, Bullet 'will watch your queries while you develop your application and notify you when you should add eager loading (N+1 queries), when you are using eager loading that is not necessary and when you should use counter cache.' The README also notes best practice directly: 'use Bullet in development mode or custom mode (staging, profile, etc.).' Running it in production would expose internal performance details to end users via alerts and footers.

## Installing Bullet and Running the Generator

Add Bullet to your Gemfile in the development group:

```ruby
gem 'bullet', group: 'development'
```

After running `bundle install`, generate the default configuration with:

```ruby
bundle exec rails g bullet:install
```

The generator creates the initializer block in config/environments/development.rb and may ask whether to include Bullet in the test environment as well. The README notes that Bullet must be added after activerecord (Rails) and mongoid in the Gemfile to ensure the hooks are registered in the correct order.

Bullet supports activerecord >= 4.0 and mongoid >= 4.0. For older versions, the README specifies that activerecord 2.x requires bullet <= 4.5.0 and activerecord 3.x requires bullet < 5.5.0. Nothing in the README documents a specific upper bound on Rails version compatibility.

The gem can also be installed directly without Bundler:

```
gem install bullet
```

However, the Gemfile approach is the standard for Rails applications because it ties the Bullet version to the project rather than the system Ruby installation.

## Configuring Notification Channels

Bullet does not enable any notification system by default. Every channel must be explicitly enabled in the initializer. The README shows a comprehensive configuration block in config/environments/development.rb. A minimal configuration that writes to the Rails log, adds a page footer, and sends logs to the Bullet log file looks like this:

```ruby
config.after_initialize do
  Bullet.enable = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
  Bullet.add_footer = true
end
```

The available channels from the README include: browser JavaScript alert (Bullet.alert), browser console logging (Bullet.console), the Bullet log file at Rails.root/log/bullet.log (Bullet.bullet_logger), the Rails log (Bullet.rails_logger), a page footer (Bullet.add_footer), and integrations with Sentry, Honeybadger, Bugsnag, AppSignal, Airbrake, Rollbar, Slack, XMPP/Jabber, and OpenTelemetry.

The footer position defaults to the bottom left corner. The README documents four valid values for Bullet.footer_position: 'bottom_left', 'bottom_right', 'top_left', and 'top_right'. This matters when the default position overlaps with browser UI elements such as Chrome's URL preview bar.

Bullet.raise is a particularly useful option in test suites: it raises an error when a problematic query is detected, causing specs to fail unless the code uses optimized queries. The README describes this as 'useful for making your specs fail unless they have optimized queries.'

## The Three Detectors and How to Disable Each

Bullet runs three independent detectors, all enabled by default. Each can be disabled separately:

```ruby
Bullet.n_plus_one_query_enable     = false
Bullet.unused_eager_loading_enable = false
Bullet.counter_cache_enable        = false
```

The n_plus_one_query detector watches for repeated association queries after a collection load. The unused_eager_loading detector catches includes() calls whose associations are loaded but never accessed in the request. The counter_cache detector identifies COUNT queries that could be replaced by a cached integer on the parent record.

Disabling individual detectors is useful when a particular pattern is known and intentional. For instance, a background job that deliberately loads records one by one for external API calls may trigger N+1 detection even though that access pattern is correct in that context. Selectively disabling the counter_cache detector avoids noise in applications where maintaining a counter cache is not worth the overhead.

The detectors work by observing ActiveRecord's query notifications through the instrumentation layer. They do not modify query execution, only observe it. This means Bullet adds overhead during development but has no effect on production behavior when not loaded.

## Safe Lists and Per-Action Skipping

Some N+1 patterns are known issues that a team has decided to accept, or they originate in third-party code outside the application's control. Bullet provides a safe list to suppress those specific notifications:

```ruby
Bullet.add_safelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
```

The safe list entry takes a type (:n_plus_one_query, :unused_eager_loading, or :counter_cache), a class_name, and an association. Once added, Bullet stops reporting that specific combination.

For controller actions where Bullet should be fully disabled, the README documents Bullet.skip. The README notes this is 'thread-safe and works correctly in multi-threaded environments like Puma,' which is important because a naive global flag would affect concurrent requests.

The safe list is most useful when upgrading to Bullet in an existing application with many pre-existing issues. Teams can add known patterns to the safe list, get the application passing cleanly, and then remove safe list entries one by one as they fix each issue. This incremental approach is more manageable than trying to fix every N+1 problem in a large codebase at once.

## Where Bullet Falls Short and What It Cannot Detect

Bullet detects patterns in ActiveRecord query instrumentation, which means it only sees what ActiveRecord sees. Raw SQL queries issued via execute() or through non-instrumented database adapters are invisible to it. Applications that bypass ActiveRecord for performance-critical queries will not get coverage for those paths.

Bullet also does not detect slow queries that are not N+1 patterns. A single query that performs a full table scan or a complex subquery without an index will not trigger any Bullet notification. Query plan analysis tools such as EXPLAIN in PostgreSQL are needed for those cases. Bullet is not a replacement for a general query profiler; it is a targeted N+1 detector.

The gem is not designed to detect pagination-related performance issues or problems that arise only under specific data volumes. An N+1 pattern that only becomes expensive with thousands of records may go unnoticed during development with small seed data sets. Bullet fires when the pattern occurs, not when it causes measurable slowness.

Alternatives take a different approach. rack-mini-profiler attaches a timing widget to each page response and shows the full query log with stack traces for every query, not just N+1 patterns. It gives broader visibility into what queries run and how long each takes, but it does not specifically identify which associations to add to includes(). For teams that want automatic detection of the specific fix to apply, Bullet's targeted approach is more actionable.

## ActiveRecord Version Support, Maintenance, and MIT License

The README documents a version matrix for ActiveRecord and Mongoid compatibility going back to activerecord 2.x. The repository includes separate Gemfiles for different Rails versions: Gemfile.rails-7.0, Gemfile.rails-7.1, Gemfile.rails-7.2, Gemfile.rails-8.0, and Gemfile.rails-8.1, along with Gemfile.mongoid, Gemfile.mongoid-8.0, and Gemfile.mongoid-9.0. This structure suggests that compatibility across Rails major versions is actively tested.

The last push to the repository was on 2026-08-29. The repository has no GitHub releases; version history is tracked through the CHANGELOG.md file. The lib/ directory holds the gem implementation, and spec/ contains the test suite. The Guardfile indicates automated test watching is supported for contributors.

The license is MIT, which permits use in commercial applications, modification, and redistribution without restriction beyond attribution. No contributors license agreement or additional terms are documented in the repository.

Upgrade cost is typically low between minor versions. The three core detectors have remained stable across versions. Changes tend to affect notification channel integrations (as new services are added) and the compatibility shims for different ActiveRecord versions. Reviewing the CHANGELOG.md before upgrading is the fastest way to check for any changes to the detection logic or configuration API.

## Conclusion

Rails and Mongoid developers who want automated detection of N+1 queries and eager loading problems during development should add Bullet to their Gemfile as a development-only dependency. Teams running only raw SQL or non-ActiveRecord ORMs get nothing from it. Before relying on it, verify that the ActiveRecord version in use meets the >= 4.0 requirement documented in the README, and check whether the notification channels you plan to use, such as Sentry or Honeybadger, are already configured in the development environment.

## FAQ

### Does Bullet work with Mongoid or only with ActiveRecord?

Bullet supports both. The README states it supports activerecord >= 4.0 and mongoid >= 4.0. Separate Gemfiles for Mongoid versions 8.0 and 9.0 are present in the repository.

### Can Bullet be used in a test environment to make specs fail on N+1 queries?

Yes. Setting Bullet.raise = true causes Bullet to raise an error when it detects a problematic query, which will fail any spec that triggers such a query. The README describes this as 'useful for making your specs fail unless they have optimized queries.' The generator may ask during installation whether to include Bullet in the test environment.

### What is the safe list in Bullet and when should it be used?

The safe list lets you suppress notifications for specific known patterns using Bullet.add_safelist with a type, class_name, and association. It is useful for N+1 patterns in third-party code outside your control, or patterns you have decided are acceptable. Adding issues to the safe list during a migration lets you silence known problems while fixing them incrementally.

## Sources

- [flyerhzm/bullet on GitHub](https://github.com/flyerhzm/bullet)
- [Issues](https://github.com/flyerhzm/bullet/issues)
- [License: MIT](https://github.com/flyerhzm/bullet/blob/main/LICENSE)
- [README](https://github.com/flyerhzm/bullet/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/flyerhzm-bullet
