ruby 114 lines · 4 tabs

Per-Request Query Budget (Detect Runaway Pages)

Shared by codesnips Jan 2026
4 tabs
module QueryBudget
  class Counter
    IGNORED = %w[SCHEMA CACHE TRANSACTION].freeze

    attr_reader :count

    def initialize
      @count = 0
      @offenders = Hash.new(0)
    end

    def call(_name, _start, _finish, _id, payload)
      return if IGNORED.include?(payload[:name])
      return if payload[:cached]

      @count += 1
      @offenders[normalize(payload[:sql])] += 1
    end

    def top_offenders(limit = 5)
      @offenders.sort_by { |_sql, n| -n }.first(limit)
    end

    private

    def normalize(sql)
      sql.to_s.gsub(/\d+/, "?").squeeze(" ").strip.slice(0, 120)
    end
  end
end
4 files · ruby Explain with highlit

A single Rails action that fires 400 SQL queries usually looks fine in development and then melts a database connection pool in production. This snippet installs a per-request query budget so runaway pages announce themselves loudly instead of silently degrading. It counts every SQL statement executed within a request and, when the count crosses a threshold, either logs a rich warning or raises so the offending code path is caught in CI and staging.

The counting happens in QueryBudget counter, a small object subscribed to ActiveSupport's sql.active_record notification. Each call increments a plain integer unless the payload is a SCHEMA or CACHE query, since those are not real round trips to the database. The counter also captures the sql and name of the heaviest offenders so the eventual report can point at the actual statements. Keeping this as a per-instance object rather than a global avoids leaking counts across threads and requests.

QueryBudget middleware wires one counter into each request's lifecycle. It stashes the counter in env so controllers can read it, subscribes for the duration of the request, and always unsubscribes in an ensure block — a leaked subscription would keep counting forever and slowly poison every subsequent request on that worker. After the downstream app returns, report! compares the count against Rails.application.config.x.query_budget and decides whether to warn or raise, driven by raise_over_budget? so production stays lenient while test and development fail fast.

ApplicationController exposes the current count through query_count and adds with_query_budget, letting a specific action tighten its own limit for a known hot path. This is the key trade-off: a global budget catches regressions broadly, while per-action budgets encode intent for endpoints that legitimately need more queries.

The main pitfall is eager-loading blind spots — a budget alone does not fix N+1s, it only surfaces them, so the fix is still includes or a batched query. Because the subscriber is threadsafe per-request and cheap, the overhead is negligible even under load, making this a practical guardrail rather than a heavyweight APM.


Related snips

Share this code

Here's the card — post it anywhere.

Per-Request Query Budget (Detect Runaway Pages) — share card
Link copied