// application_performance_monitoring

Rails APM,
built only for Rails

DeadBro is application performance monitoring for Ruby on Rails: controller traces, ActiveRecord query breakdowns, automatic N+1 detection, background job timing and error tracking — in one open-source gem, with plan-based sampling and request limits.

Rails 6.1+ · Ruby 3.0+ · ActiveJob, Sidekiq, Solid Queue · free up to 50k requests/mo

Short answer

A Rails APM is a tool that instruments a running Ruby on Rails application and reports where each request spent its time — controller action, ActiveRecord queries, view rendering, cache calls, external HTTP, and background jobs — so you can find and fix slow code with evidence instead of guesses.

What separates a Rails APM from a generic one is the vocabulary. It hooks into ActiveSupport::Notifications, the event bus Rails already emits on, so its data arrives as controller#action, SQL per request, partial render times and job timing — not anonymous spans you have to reverse-engineer.

// 01 · definition

What is a Rails APM?

Application performance monitoring (APM) is the practice of measuring a production application from the inside. Instead of checking whether the server is up, an APM records every unit of work the app performs — an HTTP request, a background job — and breaks that work down into the time spent in each layer.

For a Rails application those layers are specific and well-defined. A request enters through the Rack middleware stack, gets routed to a controller action, runs ActiveRecord queries, hits (or misses) the cache, renders templates and partials, serializes a response, and possibly enqueues jobs. Ruby's garbage collector runs somewhere in the middle of all of it. A Rails APM measures each of those stages separately, for every request, and stores the result so you can compare today against last Tuesday, or one deploy against the previous one.

That per-layer breakdown is the whole point. "The dashboard is slow" is not actionable. "DashboardController#index spends 2.1s of its 2.4s P95 in 47 near-identical SELECT users queries fired from line 24" is a fix you can ship this afternoon.

Why Rails needs its own kind of APM

Generic APM agents attach to a language runtime and report generic spans. They work, but the Rails-shaped problems fall through the gaps: an N+1 query looks like forty fast queries rather than one bug, a slow partial looks like unattributed render time, and a job that waited twenty minutes in a Sidekiq queue does not appear at all because nothing was "slow" once it ran.

Rails, unusually among frameworks, publishes its own instrumentation. Every SQL call emits sql.active_record, every render emits render_template.action_view, every controller action emits process_action.action_controller, each carrying structured payloads. A Rails-native APM subscribes to those events directly, which is why it can be both more precise and cheaper than a generic agent doing runtime patching.

Request lifecycle

Rails-shaped traces

Time attributed to the controller action, ActiveRecord, ActionView renders, cache and Redis reads, and external HTTP calls — labelled with the names you already use in your codebase.

ActiveRecord

Query-level detail

Query count and duration per request, the slowest statements with backtraces, and repeated-query patterns that indicate a missing includes or a missing index.

Async work

Jobs as first-class work

ActiveJob — on Sidekiq, Solid Queue, GoodJob or Delayed Job — traced with the same SQL and memory breakdown as web requests, plus per-queue size and latency from the job backend itself.

// 02 · signals

What a Rails APM should measure

Not all of these matter every day, but a gap in any one of them is a class of production problem you cannot see. Averages in particular hide everything: a 180ms mean response time is compatible with 5% of your users waiting four seconds.

SignalWhat it tells youRails source
Web requests
Latency percentilesP50 for the typical request, P95 and P99 for how bad your worst requests get. Track per controller action, not just app-wide.process_action.action_controller
ThroughputRequests per minute per endpoint — the multiplier that turns a 300ms regression into real cost.Rack middleware
Database timeTotal ms and query count per request. A request with 120 queries is a design problem regardless of how fast each one is.sql.active_record
View renderingTemplate and partial render time, including partials rendered in a collection loop.render_template.action_view
Cache efficiencyHit and miss ratio per store. A cache that misses is slower than no cache.cache_read.active_support
External HTTPTime spent waiting on third-party APIs — Stripe, S3, an internal service — which no amount of query tuning will fix.Net::HTTP
Background work
Job durationExecution time per job class, with the same query and memory breakdown as a web request.perform.active_job
Queue latencyHow long jobs wait before they start. The most under-monitored number in most Rails apps.Sidekiq / Solid Queue stats
Failure rateWhich job classes raise, how often, and with what exception.exception.active_job
Runtime & correctness
Memory & GCHeap growth, object allocations and GC pressure. The difference between a leak and normal churn, and the reason for most OOM restarts.GC.stat / ObjectSpace
N+1 patternsRepeated structurally-identical queries within one request or job, with the call site that produced them.sql.active_record + backtrace
ExceptionsUnhandled errors with stack trace, filtered request params, user id and occurrence counts.Rack + ActiveJob middleware
Deploy markersWhich release a change in any of the above began with.Deploy tracking
// 03 · architecture

How DeadBro works

Five stages, from the event Rails already emits to the trace on your screen. Nothing in the request path makes a network call, and nothing in the request path blocks.

01

A railtie attaches on boot

The gem registers a Railtie that inserts two Rack middlewares — one for SQL tracking, one for error capture — and subscribes to the ActiveSupport notification namespaces for SQL, views, cache, Redis, Elasticsearch, HTTP and ActiveJob. No monkey-patching of ActiveRecord or ActionController, and no per-controller code to add.

DeadBro::Railtie
2 middlewares · ~10 subscribers
02

Subscribers record events into a per-request buffer

Each notification — a SQL call, a render, a cache read, an HTTP call — appends a small record to a buffer held in the current thread. Query text is normalised (literals and bind placeholders replaced) so structurally-identical queries group, and repeats are counted as they arrive. That count is the N+1 signal.

Thread-local tracking stack
SQL normalised, never raw values
backtraces only on slow queries
03

The middleware seals the trace and filters it

When the response leaves the stack, the buffer becomes one trace: total time, the layer breakdown, the query list, memory delta, and the user id picked up from Warden. Parameters whose keys look sensitive — passwords, tokens, secrets — are replaced with [FILTERED] in-process and never leave your app.

filter → sample → enqueue
sample rate set from the dashboard
errors bypass sampling
04

Background threads batch and ship

Traces go to a bounded in-memory queue drained by a small pool of dispatcher threads, which post batches over HTTPS. The push is non-blocking: if the queue is full, the trace is dropped and counted. A circuit breaker stops the gem from even trying while DeadBro is unreachable. A monitoring outage is never your outage.

2 dispatcher threads
bounded queue, fail-open
circuit breaker on ingest errors
05

Ingest aggregates, and alerts fire on rules

Batches are rolled up into per-endpoint percentiles and per-error issues, keeping full request samples so you can drill from a P95 line into an actual trace. Alarms you define — error rate, response time, uptime, server load — route to Slack, email or Pushover, with throttling so one incident is one notification.

percentiles + issues
Slack · email · Pushover
throttled, per-environment rules
<1ms
agent overhead per request
0
network calls inside a request
<1ms
N+1 detection overhead
100%
open-source agent

What a trace looks like

Every sampled request becomes a breakdown. The layers are stacked in the order they happened, so a request that looks fine in aggregate reveals itself immediately: here, the controller is not slow — one includes is missing, and 47 identical user lookups are being fired from inside the post loop.

Each query carries its normalised SQL, count and duration; slow ones carry the backtrace into your source. Because the user id is attached, you can also start from a complaint — "this customer says it is slow" — and read their actual requests.

Request · GET /posts · P95 sample
PostsController#index 2,410ms total
Rack middleware18ms
routing + before_action41ms
Post.published (1 query)62ms
SELECT users … × 471,412ms
SELECT tags … × 12288ms
cache_read (hit 0.82)9ms
_post.html.erb × 25402ms
GC pause74ms
response + logging31ms
⚡ N+1 detected — app/views/posts/_post.html.erb:7 loads post.author per row · 47 identical queries · ≈1.7s
// 04 · setup

Adding APM to a Rails app

One gem, one generator, one environment variable. Everything else — sample rate, slow-query threshold, excluded controllers, memory and allocation tracking — is a setting on the dashboard that the gem picks up remotely, so you tune it without a deploy.

terminal
# 1. add the gem bundle add dead_bro --git https://github.com/rubydevro/dead_bro.git # 2. generate the initializer + set your key bin/rails generate dead_bro:install export DEAD_BRO_API_KEY=… # 3. deploy. requests appear within seconds. git push

The generated initializer is a single line. The railtie handles attachment on boot; there is nothing to add per controller or per job.

config/initializers/dead_bro.rb
DeadBro.configure do |cfg| cfg.api_key = ENV["DEAD_BRO_API_KEY"] end # managed from the dashboard, applied live: # sample_rate 1–100 % # slow_query_threshold_ms backtrace above this # excluded_controllers skip health checks, etc. # memory_tracking_enabled lightweight GC snapshots # local opt-in for plan-only EXPLAIN: # explain_enabled requires dead_bro ≥ 0.2.33 # explain_analyze_enabled use on 0.2.32 or older

Instrumenting your own code

Anything Rails does not emit an event for — a search call, a PDF render, a pricing calculation — can be wrapped in a DeadBro.watch block so it appears as its own span inside the request or job. Rescued exceptions you still want to see go through DeadBro.track, with whatever context helps you reproduce them.

app/services/search_service.rb
# appears as its own span in every request that hits it DeadBro.watch("search.elasticsearch", term: term) do Elasticsearch::Model.search(term, Post).records end # rescued, but still worth knowing about rescue Stripe::CardError => e DeadBro.track(e, user_id: current_user.id, plan: plan.name) # in the console: which queries does this block run, and how often? DeadBro.analyze("load dashboard") { Dashboard.build(user) }.most_queries
// 05 · diagnosis

The five bottlenecks
every Rails app hits

Rails performance problems are remarkably repetitive. Almost every slow Rails app is slow for one of these reasons, and each has a distinctive signature in a trace.

Bottleneck 01

N+1 queries

An association is loaded inside a loop, so rendering 50 rows fires 51 queries. Each query is fast, which is why this never appears in slow-query logs, and why it scales linearly with your data.

Signature: dozens of structurally identical queries in one trace.
Fix: includes / preload the association.
Bottleneck 02

Missing indexes

One query dominates the trace and gets steadily worse as the table grows. Usually a where or order on a column added in a migration that never got an index.

Signature: single query > 200ms, degrading week over week.
Fix: index the filtered/sorted column; check the query plan.
Bottleneck 03

View rendering & partials

A partial rendered per collection item, each doing a helper call or a small query. Database time looks reasonable; the time is in ActionView.

Signature: render time rivals or exceeds DB time.
Fix: collection rendering, fragment caching, fewer helper round-trips.
Bottleneck 04

Background job backlog

Jobs run fast but start late. Emails arrive twenty minutes after signup because one slow job class is monopolising the queue. Job duration monitoring alone will not show this.

Signature: queue latency rising while job duration is flat.
Fix: separate queues, more workers, or move work out of the hot queue.
Bottleneck 05

Memory bloat and GC pressure

RSS climbs across a day until the platform restarts the process. Usually a query loading hundreds of thousands of rows into objects, or a cache with no bound.

Signature: sawtooth heap growth; GC pauses inside traces.
Fix: in_batches, pluck, bounded caches, fewer allocations.
Underneath all five

You cannot fix what you cannot attribute

Each of these has a clear fix that takes an hour. The expensive part is the week spent arguing about which one it is. That is the entire argument for running an APM: it turns a debate into a line number.

DeadBro: slow queries carry their backtrace — controller, file and line.
// 06 · scope

APM vs logs vs error
tracking vs uptime

These four tools are often confused, and teams frequently buy three of them believing they have covered performance. They answer different questions.

ToolAnswersCannot tell youRails perf coverage
Uptime monitoringIs the app responding to a health check?Anything about the requests real users make.None
Log aggregationWhat happened, in order, for one request you already suspect.Which endpoints are slow across millions of requests. Aggregation is on you.Indirect
Error trackingWhich exceptions were raised, where, and for whom.Anything about requests that succeed slowly — which is most performance work.Correctness only
Infrastructure metricsCPU, memory and disk on the host or dyno.Which controller action is consuming that CPU.Symptoms only
Rails APMWhere time goes per endpoint and per job, with percentiles, traces and call sites.Host-level capacity planning — pair it with infra metrics.Primary tool

DeadBro bundles APM, error tracking and uptime checks in one product for a practical reason: they share the same instrumentation and the same context. When an exception is captured, the request that raised it — its queries, its user, its params — is already there. Correlating those across two vendors is a task nobody enjoys doing at 3am.

// 07 · evaluation

Choosing a Rails APM

Six questions worth asking any vendor, including us. The pricing model matters more than most teams expect — it determines whether monitoring costs scale with traffic or with infrastructure.

Question 01

Per-host or per-request pricing?

Per-host billing punishes autoscaling and containerisation: twenty pods for a flash sale means twenty times the monitoring bill. Per-request tracks the work your app actually did.

Question 02

Is N+1 detection automatic?

If it requires a separate gem in development, it only catches what a developer happens to exercise locally. Runtime detection in production catches what your users hit.

Question 03

Are background jobs first-class?

Ask specifically about queue latency, not just job duration. Many tools report only the latter.

Question 04

What is the measured overhead?

Ask for a number and the method used to get it. Agents that patch broadly and call out synchronously can cost several percent of your request time.

Question 05

What happens when it breaks?

A monitoring outage should be invisible to your app. Bounded queues and dropped batches, never blocking a request on a reporting endpoint.

Question 06

What leaves the process?

Query literals, params and headers can contain personal data. Normalisation and filtering should happen before anything is sent, not server-side after.

Side-by-side comparisons

Detailed feature and pricing breakdowns against the tools Rails teams usually evaluate alongside DeadBro.

// 08 · questions

Rails APM, answered

What is a Rails APM?

A Rails APM instruments a running Ruby on Rails application and reports how long each request took and where that time went — the controller action, ActiveRecord queries, view rendering, cache calls, external HTTP calls and background jobs. Unlike a generic APM, it subscribes to ActiveSupport::Notifications, so its data is expressed in Rails terms: controller#action, SQL per request, partial render times, ActiveJob timing and queue latency.

How is APM different from error tracking?

Error tracking records exceptions after they are raised. APM records timing and resource use for every request, including the ones that succeed but are slow. Most Rails performance problems — N+1 queries, missing indexes, slow partials, job backlogs — never raise an exception, so error tracking alone will not surface them. DeadBro does both in one gem, which means an exception arrives with the request that caused it.

Does a Rails APM slow down my app?

Every APM adds some work to an instrumented request. DeadBro uses Rails notifications and ships data asynchronously. Actual overhead depends on your traffic and enabled settings; measure it in your app. Optional allocation tracking is off by default.

Ask any vendor how its overhead was measured and test with your workload.

Is there a free Rails APM?

Yes — DeadBro's free tier covers 50,000 requests per month with performance traces, error tracking, N+1 detection and an uptime check, with 7 days of retention, forever and without a credit card. Paid plans start at €19/month for 2 million requests and are priced by request volume rather than per host, so autoscaling does not multiply your bill. There are no overage fees: the allowance is spread across the month and tracking pauses until the next day if you hit it. See pricing for the limits per tier.

Can it monitor Sidekiq and ActiveJob?

Yes, and it should be a requirement. Background jobs run the same ActiveRecord code as web requests and are a common source of N+1 queries and memory growth. DeadBro instruments ActiveJob — on Sidekiq, Solid Queue, GoodJob or Delayed Job — with the same SQL and memory breakdown as web requests, and reports per-queue size and latency from the backend, which is usually the number your users actually feel.

Which Rails and Ruby versions are supported?

Rails 6.1 and newer on Ruby 3.0 and later. The agent is open source (rubydevro/dead_bro) and runs under any Rack server, in multi-process and multi-threaded configurations; the dispatcher threads are per process.

How do I find N+1 queries in Rails?

In development, the Bullet gem flags them on pages you happen to visit. In production, an APM finds them at runtime by normalising query text and counting structurally-identical queries within a single request or job — which catches the ones only real data volumes and real user paths trigger.

DeadBro does this with no configuration and reports the query pattern, its count and its call site. See the N+1 detection feature for the detail.

Do I still need logs and infrastructure metrics?

Logs, yes — for the forensic detail of one specific request, and for anything your app writes deliberately. Infrastructure metrics, yes — for capacity planning and host health. What APM replaces is the practice of grepping logs to guess which endpoint is slow, and the practice of inferring application behaviour from a CPU graph.

What data leaves my application?

DeadBro sends timing data, controller and job names, stack traces, and normalised query text with literals replaced by placeholders. Supported auth integrations can attach a user ID. Parameters whose keys look like passwords, tokens, or secrets are replaced with [FILTERED] before transmission. Review any custom context you add for personal data. Retention follows your plan, and per-user deletion is available. See the GDPR page for hosting and data details.

Find out where the time goes

Create a free account, add the gem and API key, deploy, and inspect captured requests from your app.

Start free — 50k requests/mo

No credit card. No enterprise sales call. Just bundle install.