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

ScenarioMedianP95P99DB writes
Baseline (disabled)0ms0ms0ms0
Enabled, async: true1.9ms5.1ms7.7ms1+
Enabled, async: false1.8ms2.75ms5.9ms1+

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:

DatabaseTypical overheadNotes
SQLite1–3msFast for development; single-writer limitation under concurrency
PostgreSQL2–5msRecommended for production; use connection pooling
MySQL2–5msSimilar to PostgreSQL with proper indexes

What drives the overhead

Almost all overhead comes from persisting data to the database, not from instrumentation:

  • OperationSubscriber hooks into ActiveSupport::Notifications — these are events Rails fires regardless, so the subscription cost is negligible
  • Per request: one Route.find_or_create_by, one Request.create!, and one Operation.create! per tracked operation (SQL query, template render, cache read, etc.)
  • The RequestStore accumulation 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