Performance Impact
Understand Rails Pulse's performance overhead and how to minimize its impact on your application.
Overview
Rails Pulse is designed with a minimal footprint. Instrumentation happens through ActiveSupport::Notifications — events that Rails already fires — and database writes are handled via the async gem.
Benchmark Results
The following numbers were measured by running the Rails Pulse middleware stack against a passthrough Rack app (no application logic), using bin/benchmark from the Rails Pulse repository. These isolate middleware overhead from your application’s own work.
Environment: Ruby 3.3.6 · Rails 8.0.2 · SQLite3 · Apple Silicon
| Scenario | Median | P95 | P99 | DB writes |
|---|---|---|---|---|
| Baseline (disabled) | 0ms | 0ms | 0ms | 0 |
| Enabled, async: true | 1.9ms | 5.1ms | 7.7ms | 1+ |
| Enabled, async: false | 1.8ms | 2.75ms | 5.9ms | 1+ |
Note on DB writes: The “1+” reflects a minimal passthrough request with no tracked operations. In a real request with SQL queries, template rendering, and a controller action, Rails Pulse writes one additional record per operation tracked — typically 5–15 writes for an average page.
async: true vs async: false on Puma
The benchmark shows async: false as marginally faster, which deserves an explanation. With async: true, the Async { } block creates a new fiber reactor, runs the DB writes inside it, then tears it down — that setup/teardown has a small cost per request. With async: false, the writes just run directly with no fiber overhead.
In standard Puma (the default Rails web server), there is no persistent reactor, so async: true pays that cost every request without any concurrency benefit. The numbers are effectively the same for Puma users — leave async: true and don’t tune this setting.
The real benefit of async: true is on async-native servers like Falcon, where a reactor is already running and the fiber can genuinely yield during I/O, freeing the thread to handle other requests while the DB write completes.
Running your own benchmarks
# SQLite (default)
bin/benchmark --iterations=200
# PostgreSQL
DB=postgresql bin/benchmark --iterations=200
# Show history across runs
bin/benchmark --compare
Results are saved to benchmarks/results/ as timestamped JSON files so you can track changes over time or across Ruby/Rails upgrades.
Performance by Database
Write latency varies by adapter:
| Database | Typical overhead | Notes |
|---|---|---|
| SQLite | 1–3ms | Fast for development; single-writer limitation under concurrency |
| PostgreSQL | 2–5ms | Recommended for production; use connection pooling |
| MySQL | 2–5ms | Similar to PostgreSQL with proper indexes |
What drives the overhead
Almost all overhead comes from persisting data to the database, not from instrumentation:
OperationSubscriberhooks intoActiveSupport::Notifications— these are events Rails fires regardless, so the subscription cost is negligible- Per request: one
Route.find_or_create_by, oneRequest.create!, and oneOperation.create!per tracked operation (SQL query, template render, cache read, etc.) - The
RequestStoreaccumulation during the request is in-memory and adds no I/O cost
Minimizing overhead
Filter low-value routes
The biggest win is ignoring routes that don’t need monitoring:
RailsPulse.configure do |config|
config.track_assets = false # already the default
config.ignored_routes = [
"/health",
"/status",
%r{^/admin/system}
]
end
Tune data retention
Fewer records means faster cleanup jobs and smaller tables:
RailsPulse.configure do |config|
config.full_retention_period = 7.days
config.max_table_records = {
rails_pulse_requests: 25_000,
rails_pulse_operations: 50_000,
rails_pulse_queries: 5_000
}
end
Use a separate database
For high-traffic apps, isolating Rails Pulse writes from your main database removes contention from your primary connection pool:
RailsPulse.configure do |config|
config.connects_to = {
database: { writing: :rails_pulse, reading: :rails_pulse }
}
end
Disable in test
RailsPulse.configure do |config|
config.enabled = !Rails.env.test?
end
Relative impact by request speed
- Fast requests (< 50ms): Overhead is proportionally larger (3–10%)
- Average requests (100–500ms): Moderate impact (0.5–2%)
- Slow requests (> 1000ms): Negligible (< 0.2%)
The slower your requests, the less Rails Pulse overhead matters.
Next Steps
- Deployment Modes — run the dashboard as a separate process to isolate its resource usage from your main app
- Database Setup — configure a dedicated Rails Pulse database for high-traffic applications