ruby 66 lines · 3 tabs

Guard Against Slow Queries with statement_timeout

Shared by codesnips Jan 2026
3 tabs
module StatementTimeout
  extend ActiveSupport::Concern

  class TimeoutExceeded < StandardError; end

  def with_statement_timeout(milliseconds)
    connection = ActiveRecord::Base.connection
    previous = connection.select_value("SHOW statement_timeout")

    connection.execute("SET statement_timeout = #{connection.quote(milliseconds)}")
    yield
  rescue ActiveRecord::QueryCanceled => e
    raise TimeoutExceeded, "query exceeded #{milliseconds}ms budget: #{e.message}"
  ensure
    if connection
      restore = connection.quote(previous.presence || "0")
      connection.execute("SET statement_timeout = #{restore}")
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how to protect a Rails application from runaway Postgres queries by scoping a statement_timeout to a block of code, rather than setting one global value. Postgres exposes statement_timeout as a session-level setting: any statement that runs longer than the configured milliseconds is aborted by the server with a QueryCanceled error. The trick is to set it on the current connection, run some work, and always restore the previous value — even when the query blows up.

In StatementTimeout concern, with_statement_timeout captures the connection's existing statement_timeout with SHOW statement_timeout, applies the requested value via SET, and uses an ensure block to put the old value back. Wrapping the setting in the same connection matters: ActiveRecord's connection is checked out from the pool for the current thread, so the timeout applies only to statements issued on that checkout. Values are passed through quote to avoid injecting arbitrary SQL into the SET command. QueryCanceled is caught and re-raised as a domain-specific TimeoutExceeded so callers can distinguish a deliberate guard from an unexpected failure.

ReportsController demonstrates the request-facing use case: an expensive aggregation is wrapped in with_statement_timeout(2_000) so a single slow report can never hold a web worker hostage for more than two seconds. When the guard trips, rescue_from turns it into a clean 503 with a Retry-After header instead of a stack trace or a hung Puma thread.

ExportBatchJob shows the opposite end of the spectrum: background work legitimately needs longer budgets, so it uses a generous 30_000 ms timeout. This illustrates the core trade-off — a timeout that is too tight kills valid work, while one that is too loose lets a pathological query exhaust the connection pool. Scoping the value per operation lets each context pick its own budget.

A key pitfall is restoring the original setting: without the ensure, a leaked timeout would silently apply to every later query reusing that pooled connection. Because the reset runs even on cancellation, the connection returns to the pool clean. This pattern is worth reaching for whenever untrusted filters, ad-hoc analytics, or user-driven queries could otherwise run unbounded.


Related snips

Share this code

Here's the card — post it anywhere.

Guard Against Slow Queries with statement_timeout — share card
Link copied