Monitor your database's health by tracking active sessions. When load gets too high, shed work to keep your app running.
This gem was inspired by Simon Eskildsen, who described a similar system in place at Shopify.
Databases slow down when they have too many active queries. This gem helps you:
- Shed load safely. Skip low-priority work when the database is busy.
- Protect your app. Return 503 errors instead of timing out, which allows higher-priority work to get through.
The gem counts active database sessions. It compares this count to your database's vCPU count. When active sessions exceed a threshold, the database is "unhealthy."
Add to your Gemfile:
gem "activerecord-health"Then run:
bundle install# config/initializers/activerecord_health.rb
ActiveRecord::Health.configure do |config|
config.vcpu_count = 16 # Required: your database server's vCPU count
config.cache = Rails.cache # Required: any ActiveSupport::Cache store
endNow check if your database is healthy:
ActiveRecord::Health.ok?
# => trueActiveRecord::Health.configure do |config|
# Required settings
config.vcpu_count = 16 # Number of vCPUs on your database server
config.cache = Rails.cache # Cache store for health check results
# Optional settings
config.threshold = 0.75 # Max healthy load (default: 0.75)
config.cache_ttl = 60 # Cache duration in seconds (default: 60)
endImportant
You must set vcpu_count and cache. The gem raises an error without them.
The threshold is the maximum healthy load as a ratio of vCPUs.
With vcpu_count = 16 and threshold = 0.75:
- Up to 12 active sessions = healthy (12/16 = 0.75)
- More than 12 active sessions = unhealthy
# Returns true if database is healthy
ActiveRecord::Health.ok?
# Get current load as a percentage
ActiveRecord::Health.load_pct
# => 0.5 (50% of vCPUs in use)Use sheddable to skip work when the database is overloaded:
ActiveRecord::Health.sheddable do
GenerateReport.perform(user_id: current_user.id)
endUse sheddable_pct for different priority levels:
# High priority: only run below 50% load
ActiveRecord::Health.sheddable_pct(pct: 0.5) do
BulkImport.perform(data)
end
# Low priority: only run below 90% load
ActiveRecord::Health.sheddable_pct(pct: 0.9) do
SendAnalyticsEmail.perform(user_id: current_user.id)
endReturn 503 when the database is overloaded:
class ReportsController < ApplicationController
before_action :check_database_health
private
def check_database_health
return if ActiveRecord::Health.ok?
render json: { error: "Service temporarily unavailable" },
status: :service_unavailable
end
endRetry jobs when the database is unhealthy:
# config/initializers/sidekiq.rb
class DatabaseHealthMiddleware
THROTTLED_QUEUES = %w[reports analytics bulk_import].freeze
def call(_worker, job, _queue)
if THROTTLED_QUEUES.include?(job["queue"]) && !ActiveRecord::Health.ok?
raise "Database unhealthy, try again later"
end
yield
end
end
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add DatabaseHealthMiddleware
end
endThis uses Sidekiq's exponential backoff retries to push load into the future.
Pass the model class that connects to your database:
# Check the primary database (default)
ActiveRecord::Health.ok?
# Check a specific database
ActiveRecord::Health.ok?(model: AnimalsRecord)Configure each database separately:
ActiveRecord::Health.configure do |config|
config.vcpu_count = 16 # Default for primary database
config.cache = Rails.cache
config.for_model(AnimalsRecord) do |db|
db.vcpu_count = 8
db.threshold = 0.5
end
end| Database | Supported |
|---|---|
| PostgreSQL 10+ | Yes |
| MySQL 5.1+ | Yes |
| MySQL 8.0.22+ | Yes (uses performance_schema) |
| MariaDB | Yes |
| SQLite | No |
Send load data to Datadog, StatsD, or other tools. The gem fires an event each time it checks the database:
ActiveSupport::Notifications.subscribe("health_check.activerecord_health") do |*, payload|
StatsD.gauge("db.load_pct", payload[:load_pct], tags: ["db:#{payload[:database]}"])
StatsD.gauge("db.active_sessions", payload[:active_sessions], tags: ["db:#{payload[:database]}"])
endTip
Start by tracking load_pct for a few days. This helps you learn what "normal" looks like before you set thresholds.
The event fires only when the gem runs a query. It does not fire when reading from cache.
Event payload:
| Key | Description |
|---|---|
database |
Connection name |
load_pct |
Load as a ratio (0.0 to 1.0+) |
active_sessions |
Number of active sessions |
Add convenience methods to connections and models:
require "activerecord/health/extensions"
ActiveRecord::Base.connection.healthy?
# => true
ActiveRecord::Base.connection.load_pct
# => 0.75
ActiveRecord::Base.database_healthy?
# => trueThis gem is simple by design. Keep these limits in mind:
- Errors look like overload. The health check query can fail for many reasons: network problems, DNS issues, or connection pool limits. When this happens, the gem marks the database as unhealthy. It caches this result for
cache_ttlseconds. This can cause load shedding even when the database is fine. - Session counts can be wrong. The gem assumes many active sessions means the CPU is busy. But sessions can be active while waiting on locks, disk reads, or slow clients. The database may have room to spare, but the gem still reports it as unhealthy.
MIT License. See LICENSE for details.