β‘ RailsVitals
The Rails gem that made me understand performance.
RailsVitals is a zero-dependency Rails Engine that instruments every request in your app and surfaces performance diagnostics: N+1 queries, slow SQL, fat callbacks, and health scores through an embedded admin UI and an injectable panel overlay.
It doesn't just tell you something is wrong. It shows you why, where, and what to do about it.
Screenshots
Full admin UI at
/rails_vitalswith Dashboard, Request History, Request Detail with Query DNA, Endpoint Heatmap, Per-Model Breakdown, N+1 Patterns and Association Map.
Features
π΄ Per-Request Health Score
Every request gets a score from 0β100 based on query volume and N+1 severity. Scores are color-coded: Healthy (90β100), Acceptable (70β89), Warning (50β69), Critical (0β49).
𧬠Query DNA β Visual SQL Fingerprinting
Every query in a request is decomposed into color-coded tokens: SELECT *, WHERE fk =, IN (...), JOIN, ORDER BY, OFFSET, and more. Click any query to expand its DNA. Click any token to read an education card explaining what it means, why it matters, and how to fix it.
πΊοΈ Association Map
A live SVG diagram of your ActiveRecord model graph, annotated with N+1 status, query counts, average query time, foreign key names, and index status on every edge. Nodes light up red when N+1 patterns are detected. Dashed edges signal missing indexes on foreign keys. Click any node to open a detail panel with fix suggestions and links to affected requests.
π₯ Endpoint Heatmap
A ranked table of every endpoint in your app sorted by worst average health score. Columns: average score, hit count, average query count, average DB time, average callback time, N+1 frequency. The worst offenders surface immediately.
π Per-Model Breakdown
Which ActiveRecord models are hammering your database? The model breakdown aggregates queries by table, shows total query count, total DB time, average query time, and the endpoints responsible.
π N+1 Pattern Detector
Cross-request N+1 aggregation using normalized SQL fingerprinting. Each pattern shows total occurrences, affected endpoints, and a concrete fix suggestion generated by reflecting on your actual ActiveRecord associations e.g. Post.includes(:likes), not a generic hint.
π₯ Impact Simulator
Each N+1 pattern has a detail page showing affected requests, estimated query savings, and a generated migration-ready fix. See the blast radius before you write a line of code.
π Callback Map
Every ActiveRecord callback (before_save, after_create, before_validation, etc.) is timed and grouped by model in the Request Detail view. Expensive callbacks surface immediately β including hidden side effects like callbacks that trigger additional queries.
π‘οΈ Injected Panel
A collapsible overlay injected into every HTML response showing the current request's score, query count, DB time, N+1 count, and callback time. Zero configuration. Disappears in production.
Installation
Add to your Gemfile:
gem "rails_vitals", group: :developmentRun:
bundle installMount the engine in config/routes.rb:
mount RailsVitals::Engine, at: "/rails_vitals"That's it. Visit /rails_vitals and start browsing your app.
Configuration
RailsVitals works out of the box with sensible defaults. To customize, add an initializer:
# config/initializers/rails_vitals.rb
RailsVitals.configure do |config|
# Enable/disable instrumentation entirely
config.enabled = !Rails.env.production?
# Ring buffer size β how many requests to keep in memory
config.store_size = 200
# Query count thresholds for scoring
config.query_warn_threshold = 10 # above this β score starts dropping
config.query_critical_threshold = 25 # above this β score = 0
# DB time thresholds (ms) for slow query detection
config.db_time_warn_ms = 100
config.db_time_critical_ms = 500
# Admin UI authentication
# :none β no auth (default for development)
# :basic β HTTP Basic Auth
# lambda β custom auth logic
config.auth = :none
# Basic auth credentials (if auth: :basic)
config.basic_auth_username = "admin"
config.basic_auth_password = "secret"
# Custom auth lambda (if auth: :lambda)
# config.auth = ->(request) { request.session[:admin] == true }
endAdmin UI
Navigate to /rails_vitals to access the full admin interface.
| Page | Path | Description |
|---|---|---|
| Dashboard | /rails_vitals |
Score distribution, health trend, query volume |
| Requests | /rails_vitals/requests |
Full request history with filters |
| Request Detail | /rails_vitals/requests/:id |
Queries, Query DNA, Callback Map, Score Projection |
| Heatmap | /rails_vitals/heatmap |
Endpoints ranked by worst health score |
| Models | /rails_vitals/models |
Per-model query breakdown |
| N+1 Patterns | /rails_vitals/n_plus_ones |
Cross-request N+1 aggregation with fix suggestions |
| Association Map | /rails_vitals/associations |
Live SVG model graph with N+1 and index annotations |
How Scoring Works
RailsVitals scores each request using a CompositeScorer with two weighted dimensions:
Score = (QueryScore Γ 40%) + (N+1Score Γ 60%)
QueryScore (40%) β penalizes query volume:
- β€
query_warn_thresholdqueries β 100 - β₯
query_critical_thresholdqueries β 0 - Between thresholds β linear interpolation
N+1Score (60%) β penalizes detected patterns:
- 0 patterns β 100
- 1 pattern β 75
- 2 patterns β 50
- 3 patterns β 25
- 4+ patterns β 0
N+1 patterns are weighted more heavily because they represent an architectural problem that grows worse as data scales not just a snapshot of current query volume.
Query DNA Token Reference
| Token | Color | Risk | What it means |
|---|---|---|---|
SELECT * |
Blue | β Warning | Fetches all columns β consider .select(:id, :name)
|
SELECT |
Blue | β Healthy | Specific column selection |
COUNT(*) |
Amber | β Warning | Aggregation in a loop = N+1 variant |
AGGREGATE |
Amber | β Warning | SUM/AVG/MIN/MAX in a loop |
FROM |
Green | β Healthy | Identifies the model being queried |
WHERE fk = |
Red | π΄ Danger | Single FK lookup β the N+1 signature |
WHERE |
Orange | β Neutral | Filter condition β check for missing index |
IN (...) |
Green | β Healthy | Batch lookup β eager loading is working |
INNER JOIN |
Purple | β Neutral |
.joins() β note: association not loaded |
LEFT JOIN |
Purple | β Neutral |
.eager_load() or .left_joins()
|
ORDER BY |
Cyan | β Warning | Ensure sort column has an index |
LIMIT |
Gray | β Healthy | Good β always paginate |
OFFSET |
Red | β Warning | O(n) at scale β consider cursor pagination |
GROUP BY |
Cyan | β Neutral | Consider counter cache for frequent use |
N+1 Fix Suggestions
RailsVitals generates fix suggestions by reflecting on your actual ActiveRecord associations, not by guessing. Given a detected pattern:
SELECT "likes".* FROM "likes" WHERE "likes"."post_id" = ?It extracts the foreign key (post_id), infers the owner model (Post), reflects on Post.reflect_on_all_associations, and generates:
Post.includes(:likes)Real associations, real fix, zero guesswork.
Association Map β Reading the Diagram
- π’ Green node β model is queried, no N+1 detected
- π΄ Red node β N+1 patterns detected on this model's associations
- β¬ Gray node β model exists but hasn't been queried in recent requests
- Solid edge β foreign key is indexed β
- Dashed edge β foreign key is missing an index β οΈ
-
Orange edge β
has_many/has_one -
Purple edge β
belongs_to - Red edge β association has active N+1 patterns
Click any node to open the detail panel with query stats, association breakdown, fix suggestions, and links to affected requests.
Architecture
RailsVitals is a mountable Rails Engine with zero runtime dependencies beyond Rails itself.
rails_vitals/
βββ lib/
β βββ rails_vitals/
β βββ engine.rb # Mountable engine
β βββ configuration.rb # Config object
β βββ collector.rb # Thread-local request collector
β βββ store.rb # Thread-safe in-memory ring buffer
β βββ request_record.rb # Immutable request snapshot
β βββ panel_renderer.rb # Injected HTML panel
β βββ sse_writer.rb # Server-Sent Events writer
β βββ notifications/
β β βββ subscriber.rb # AS::Notifications SQL + controller hooks
β βββ instrumentation/
β β βββ callback_instrumentation.rb # Module prepend for AR callbacks
β βββ analyzers/
β β βββ sql_tokenizer.rb # Query DNA tokenizer
β β βββ n_plus_one_aggregator.rb # Cross-request N+1 aggregation
β β βββ association_mapper.rb # AR reflection + SVG layout
β βββ scorers/
β β βββ base_scorer.rb
β β βββ query_scorer.rb # 40% weight
β β βββ n_plus_one_scorer.rb # 60% weight
β β βββ composite_scorer.rb
β βββ middleware/
β βββ panel_injector.rb # Rack middleware for panel injection
βββ app/
βββ controllers/rails_vitals/
β βββ dashboard_controller.rb
β βββ requests_controller.rb
β βββ heatmap_controller.rb
β βββ models_controller.rb
β βββ n_plus_ones_controller.rb
β βββ associations_controller.rb
β βββ live_controller.rb
βββ views/rails_vitals/
βββ dashboard/
βββ requests/
βββ heatmap/
βββ models/
βββ n_plus_ones/
βββ associations/
βββ live/
Key architectural decisions:
- Zero JS dependencies β no Chartkick, no D3, no Chart.js. Tables for data, SVG for diagrams, vanilla JS for interactions.
- Thread-local Collector β instrumentation state is stored per-thread, never shared between concurrent requests.
- In-memory ring buffer β the Store keeps the last N requests in memory. No database writes, no schema migrations.
-
Module prepend for callbacks β callback instrumentation wraps
ActiveRecord::Base#run_callbacksviaModule#prepend. No TracePoint, no monkey-patching. - ActiveSupport::Notifications β SQL and controller events are captured via the standard Rails instrumentation bus.
Philosophy
Most Rails performance tools tell you what is slow. RailsVitals tells you why it is slow and what the right mental model is.
Every feature is designed around a teaching moment:
- Query DNA turns SQL into a readable fingerprint with explanations for each token
- The Association Map connects your data model structure to live performance data
- N+1 fix suggestions come from your actual associations, not generic advice
- Score Projection lets you understand the impact of a fix before writing code
- The Impact Simulator shows blast radius, how many requests are affected, how many queries would be saved
The goal is not just a faster app. The goal is a developer who understands why the app was slow and how to prevent it next time.
Requirements
- Ruby 3.0+
- Rails 7.0+
- PostgreSQL (some internal query filters are PostgreSQL-specific)
Development
git clone https://github.com/your-username/rails_vitals
cd rails_vitals
bundle installTo test against a real app, add to your Gemfile with a local path:
gem "rails_vitals", path: "../rails_vitals"Contributing
Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
License
The gem is available as open source under the terms of the MIT License.
Author
Built by David









