0.0
The project is in a healthy, maintained state
Bulletproof detects ActiveRecord includes that load too many records, both statically via source analysis and at runtime via ActiveSupport::Notifications.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

Bulletproof

Bulletproof detects ActiveRecord memory problems caused by over-eager includes.

  • Static analysis — Scans Ruby source files and flags includes calls that load unbounded record sets
  • Runtime monitoring — Measures actual record counts and GC pressure per request, and warns when thresholds are exceeded

Installation

# Gemfile
gem "bulletproof", group: :development
bundle install

Static Analysis

CLI

# Analyze a directory
bundle exec bulletproof app/

# Analyze a single file
bundle exec bulletproof app/models/user.rb

# Override thresholds
bundle exec bulletproof app/ --max-includes-depth 3 --max-associations 5

Output (violations found):

[WARNING] app/models/post.rb:12 — nesting depth 3 (limit: 2), no record-limiting method (limit / find, etc.) in chain
[WARNING] app/controllers/users_controller.rb:45 — 4 associations (limit: 3), no record-limiting method in chain

2 violation(s) found.

Output (clean):

No violations found.

The exit code is 0 when clean and 1 when violations are found, making it easy to integrate into CI.

Programmatic usage

report = Bulletproof.analyze("app/")

if report.ok?
  puts "No violations found."
else
  report.violations.each do |v|
    puts "[#{v.severity.upcase}] #{v.file}:#{v.line}#{v.message}"
  end
end

How detection works

A call to includes is only flagged when both conditions are true:

  1. The nesting depth or association count exceeds the configured threshold
  2. No record-limiting method (limit, find, first, page, etc.) appears anywhere in the method chain
# Flagged — unbounded full-table load
User.includes(posts: { comments: :author }).all
Post.includes(:user, :comments, :tags, :likes)

# Safe — record count is bounded
User.includes(posts: { comments: :author }).limit(10)
User.includes(posts: { comments: :author }).page(1).per(20)

Runtime Monitoring (Rails)

Setup

# config/initializers/bulletproof.rb
Bulletproof.configure do |c|
  c.enabled = Rails.env.development?

  # ---- Thresholds ----------------------------------------------------------

  # Max records loaded at once per model (default: 1_000)
  c.max_records_per_model = 1_000

  # Max total records loaded across all models per request (default: 5_000)
  # find_each / in_batches loads are excluded from this count
  c.max_total_records = 5_000

  # Max GC runs per request (default: nil = disabled)
  # GC frequency varies widely between apps; set explicitly if needed
  # c.max_gc_runs_per_request = 10

  # ---- Notifiers -----------------------------------------------------------

  # Log to Rails.logger.warn (default: true)
  c.rails_logger = true

  # Inject console.warn into HTML responses (default: true)
  # Visible in the browser's Console tab (F12)
  c.console = true

  # Inject a floating overlay panel into HTML responses (default: false)
  # Visible on the page without opening DevTools
  c.alert = true

  # Append warnings to a log file (default: nil = disabled)
  # c.log_file = Rails.root.join("log/bulletproof.log").to_s

  # Custom notifier callable — for Slack, etc. (default: nil = disabled)
  # c.notifier = ->(w) { SlackNotifier.ping(w.message) }
end

Setting enabled = true causes the Railtie to automatically insert the Rack middleware. You do not need to call config.middleware.use manually.

Notifiers

Key Default Description
rails_logger true Calls Rails.logger.warn for each warning
console true Injects console.warn before </body>. Visible in the browser Console tab
alert false Injects a floating overlay panel before </body>. Dismissible with ✕
log_file nil (disabled) Appends timestamped warnings to the specified file path
notifier nil (disabled) Callable receiving a RuntimeWarning. Use for Slack, webhooks, etc.

Warning output example

[Bulletproof] Post: loaded 5,200 records at once (limit: 1,000)
  → app/controllers/posts_controller.rb:15:in 'index'

[Bulletproof] Total records loaded in this request: 7,800 (limit: 5,000) [Post: 5,200, Comment: 2,600]

Warning types

Type Condition Related config key
:mass_instantiation A single model loaded more than max_records_per_model records in one batch max_records_per_model
:high_total_records Total records across all models exceeded max_total_records (batch loads excluded) max_total_records
:gc_pressure GC ran more than max_gc_runs_per_request times during the request max_gc_runs_per_request

find_each / in_batches

Batch processing is intentional and memory-safe, so Bulletproof does not warn on it.

# No warning — find_each loads records in bounded batches
Post.find_each(batch_size: 500) { |post| process(post) }

# Warning — all records loaded into memory at once
Post.all.to_a

How it works

Bulletproof subscribes to the instantiation.active_record ActiveSupport notification for the duration of each request. It accumulates record counts per model and uses caller_locations to identify the application code line responsible for each load. Subscription is scoped to the current thread via Thread.current, so parallel requests in multi-threaded servers (e.g. Puma) do not interfere with each other.


Configuration reference

Key Default Description
enabled false Enable runtime monitoring
max_includes_depth 2 Static: max includes nesting depth
max_associations 3 Static: max associations per includes call
max_records_per_model 1_000 Runtime: max records loaded at once per model
max_total_records 5_000 Runtime: max total records per request (batch loads excluded)
max_gc_runs_per_request nil (disabled) Runtime: max GC runs per request
rails_logger true Notifier: output to Rails.logger.warn
console true Notifier: inject console.warn into HTML
alert false Notifier: inject overlay panel into HTML
log_file nil (disabled) Notifier: append to log file
notifier nil (disabled) Notifier: custom callable receiving RuntimeWarning

Requirements

  • Ruby 3.0+
  • Rails 6.0+ (runtime monitoring only)

License

MIT License