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.
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.
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.
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.
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.
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.
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.
| Signal | What it tells you | Rails source |
|---|---|---|
| Web requests | ||
| Latency percentiles | P50 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 |
| Throughput | Requests per minute per endpoint — the multiplier that turns a 300ms regression into real cost. | Rack middleware |
| Database time | Total 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 rendering | Template and partial render time, including partials rendered in a collection loop. | render_template.action_view |
| Cache efficiency | Hit and miss ratio per store. A cache that misses is slower than no cache. | cache_read.active_support |
| External HTTP | Time spent waiting on third-party APIs — Stripe, S3, an internal service — which no amount of query tuning will fix. | Net::HTTP |
| Background work | ||
| Job duration | Execution time per job class, with the same query and memory breakdown as a web request. | perform.active_job |
| Queue latency | How long jobs wait before they start. The most under-monitored number in most Rails apps. | Sidekiq / Solid Queue stats |
| Failure rate | Which job classes raise, how often, and with what exception. | exception.active_job |
| Runtime & correctness | ||
| Memory & GC | Heap 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 patterns | Repeated structurally-identical queries within one request or job, with the call site that produced them. | sql.active_record + backtrace |
| Exceptions | Unhandled errors with stack trace, filtered request params, user id and occurrence counts. | Rack + ActiveJob middleware |
| Deploy markers | Which release a change in any of the above began with. | Deploy tracking |
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.
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.
2 middlewares · ~10 subscribers
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.
SQL normalised, never raw values
backtraces only on slow queries
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.
sample rate set from the dashboard
errors bypass sampling
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.
bounded queue, fail-open
circuit breaker on ingest errors
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.
Slack · email · Pushover
throttled, per-environment rules
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.
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.
The generated initializer is a single line. The railtie handles attachment on boot; there is nothing to add per controller or per job.
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.
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.
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.
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.
Fix: index the filtered/sorted column; check the query plan.
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.
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.
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.
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.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.
| Tool | Answers | Cannot tell you | Rails perf coverage |
|---|---|---|---|
| Uptime monitoring | Is the app responding to a health check? | Anything about the requests real users make. | None |
| Log aggregation | What happened, in order, for one request you already suspect. | Which endpoints are slow across millions of requests. Aggregation is on you. | Indirect |
| Error tracking | Which exceptions were raised, where, and for whom. | Anything about requests that succeed slowly — which is most performance work. | Correctness only |
| Infrastructure metrics | CPU, memory and disk on the host or dyno. | Which controller action is consuming that CPU. | Symptoms only |
| Rails APM | Where 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.
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.
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.
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.
Are background jobs first-class?
Ask specifically about queue latency, not just job duration. Many tools report only the latter.
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.
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.
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.
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/moNo credit card. No enterprise sales call. Just bundle install.