← Back to list

πŸš€ Using Bullet Gem the Right Way for Maximum Efficiency in Ruby on Rails

⚑ Stop N+1 Queries Before They Destroy Your Rails App

Ravi Prakash Β· 2026-06-03 15:07 Β· 0 claps Β· 3.8 min read paywalled
#ruby #ruby-on-rails #ruby-on-rails-development #rails #software-development
Open on Medium β†—
Wiki topics: 🌐 · Web Development

πŸš€ Using Bullet Gem the Right Way for Maximum Efficiency in Ruby on Rails

⚑ Stop N+1 Queries Before They Destroy Your Rails App

If you’ve ever opened your Rails logs and seen dozens (or hundreds 😱) of repeated SQL queries, congratulations β€” you’ve met the infamous N+1 Query Problem.

Rails makes database interactions beautifully simple, but without optimization, performance can silently collapse as your application grows.

That’s where the legendary Ruby gem comes in:

πŸ’Ž What is Bullet Gem?

The Bullet gem helps detect:

βœ… N+1 queries βœ… Unused eager loading βœ… Missing counter caches

It acts like a performance watchdog 🐢 for your Rails application.

Instead of discovering slow pages in production, Bullet warns you during development before users suffer.

🧠 Understanding the N+1 Problem

Imagine this simple code:

@posts = Post.all

And in the view:

<% @posts.each do |post| %>
  <%= post.user.name %>
<% end %>

Looks innocent, right?

But internally Rails executes:

SELECT * FROM posts;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
...

This becomes:

πŸ‘‰ 1 query for posts πŸ‘‰ N queries for users

Hence:

❌ N+1 Query

With 100 posts, Rails may execute 101 queries 🀯

πŸ”₯ Step 1 β€” Install Bullet Gem

Add Bullet to your Gemfile:

group :development do
  gem 'bullet'
end

Then run:

bundle install

βš™οΈ Step 2 β€” Configure Bullet Properly

Open:

config/environments/development.rb

Add:

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

🎯 What Each Configuration Does

ConfigurationPurposeBullet.enableEnables BulletBullet.alertBrowser popup alertsBullet.consoleLogs warnings in browser consoleBullet.bullet_loggerCreates bullet.logBullet.rails_loggerShows warnings in Rails logs

🚨 Step 3 β€” Detect N+1 Queries

Suppose we have:

class Post < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
  has_many :posts
end

Controller:

def index
  @posts = Post.all
end

View:

<% @posts.each do |post| %>
  <%= post.user.name %>
<% end %>

Bullet will immediately warn:

USE eager loading detected
Post => [:user]
Add to your query: .includes([:user])

πŸ”₯ Amazing.

βœ… Step 4 β€” Fix N+1 Queries Correctly

Update controller:

def index
  @posts = Post.includes(:user)
end

Now Rails performs:

SELECT * FROM posts;
SELECT * FROM users WHERE id IN (1,2,3,4);

Only 2 queries πŸŽ‰

Huge performance boost.

🧠 includes vs preload vs eager_load

Many Rails developers misuse these.

Let’s clarify πŸ‘‡

1️⃣ includes

Most commonly used.

Post.includes(:user)

Rails intelligently decides whether to use:

  • Separate queries
  • JOIN query

depending on usage.

βœ… Best default choice.

2️⃣ preload

Always uses separate queries.

Post.preload(:user)

Useful when JOINs become expensive.

3️⃣ eager_load

Always performs LEFT OUTER JOIN.

Post.eager_load(:user)

Useful when filtering/sorting on associated tables.

Example:

Post.eager_load(:user)
    .where(users: { active: true })

πŸš€ Pro Tip β€” Avoid Overusing includes

Bad:

Post.includes(:user, :comments, :likes, :tags)

Why bad? 😬

Because loading unnecessary associations increases:

❌ Memory usage ❌ Query complexity ❌ Object allocation

βœ… Best Practice

Only eager load what you actually use.

Good:

Post.includes(:user)

πŸ” Step 5 β€” Detect Unused Eager Loading

Suppose:

@posts = Post.includes(:user)

But view:

<%= post.title %>

You never use user.

Bullet warns:

AVOID eager loading detected
Remove from your query: .includes([:user])

This is INCREDIBLY useful for memory optimization 🧠

⚑ Step 6 β€” Counter Cache Optimization

Imagine:

post.comments.count

inside a loop.

Rails fires COUNT query repeatedly 😭

❌ Without Counter Cache

SELECT COUNT(*) FROM comments WHERE post_id = ?

again and again.

βœ… Add Counter Cache

Migration:

add_column :posts, :comments_count, :integer, default: 0

Model:

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

Now Rails automatically maintains:

post.comments_count

No extra queries πŸš€

πŸ§ͺ Step 7 β€” Make Bullet Fail Your Tests

Advanced teams do this πŸ’ͺ

Add in test environment:

Bullet.enable = true
Bullet.raise  = true

Now tests fail whenever N+1 queries appear.

This creates a strong performance culture πŸ”₯

πŸ—οΈ Real-World Example

Imagine an admin dashboard:

@companies = Company.all

View:

<% @companies.each do |company| %>
  <%= company.users.count %>
  <%= company.threats.count %>
<% end %>

This can generate:

❌ Hundreds of SQL queries.

βœ… Optimized Version

@companies = Company.includes(:users, :threats)

With counter cache:

company.users_count
company.threats_count

Result:

⚑ Faster dashboard ⚑ Lower DB load ⚑ Better scalability

🧠 Bullet Gem Best Practices

βœ… 1. Enable Only in Development

Never use Bullet in production.

group :development do
  gem 'bullet'
end

βœ… 2. Review Every Warning Carefully

Not every warning should blindly be fixed.

Sometimes eager loading increases memory more than query savings.

Optimization is balance βš–οΈ

βœ… 3. Benchmark Before and After

Use:

rack-mini-profiler

or:

benchmark-ips

to validate improvements.

βœ… 4. Combine with Proper Indexing

Even optimized queries need indexes.

Example:

add_index :comments, :post_id

🧰 Amazing Gems to Use Alongside Bullet

GemPurposerack-mini-profilerRequest profilingskylightPerformance monitoringnewrelic_rpmAPM monitoringprosopiteAlternative N+1 detector

πŸ”₯ Common Mistakes Developers Make

❌ Adding includes Everywhere

This causes memory bloat.

❌ Ignoring Bullet Warnings

Tiny inefficiencies become massive at scale.

❌ Using count Instead of size

post.comments.count

always hits DB.

Better:

post.comments.size

If association loaded, no query needed πŸš€

⚑ Bonus Optimization Trick

Use:

select

to fetch only needed columns.

Instead of:

User.all

use:

User.select(:id, :name)

Less memory. Faster objects.

🎯 Final Recommended Bullet Configuration

config.after_initialize do
  Bullet.enable        = true
  Bullet.alert         = true
  Bullet.console       = true
  Bullet.bullet_logger = true
  Bullet.rails_logger  = true
  Bullet.add_footer    = true
end

🏁 Final Thoughts

The Bullet gem is not just a debugging tool.

It teaches you how ActiveRecord actually behaves internally.

Teams that actively use Bullet usually build:

βœ… Faster applications βœ… Scalable architectures βœ… Healthier databases βœ… Better engineering culture

Performance is not something you add later.

Performance is an engineering habit πŸ’Ž

And Bullet helps you build that habit every single day.

πŸš€ Remember

β€œMake it work, make it right, then make it fast.”

Ruby on Rails gives developer happiness 😊

Bullet ensures users stay happy too ⚑


메타데이터
post_id
76a3a9db1ce0
slug
using-bullet-gem-the-right-way-for-maximum-efficiency-in-ruby-on-rails-76a3a9db1ce0
url
https://medium.com/@raviskit2012/using-bullet-gem-the-right-way-for-maximum-efficiency-in-ruby-on-rails-76a3a9db1ce0
canonical_url
https://medium.com/@raviskit2012/using-bullet-gem-the-right-way-for-maximum-efficiency-in-ruby-on-rails-76a3a9db1ce0
author_url
https://medium.com/@raviskit2012
status
ok
fetched_at
2026-06-09 15:37:30