Rails health check endpoint and what /up actually checks

Rails 8 already serves a health check at /up, but it only returns 200 when the app boots without raising, so if you want the database checked you write your own controller and return 503 when the query fails.

Rails has shipped a health check since 7.1, and every app generated on Rails 8 still has it. One line sits near the top of config/routes.rb: get "up" => "rails/health#show", as: :rails_health_check. Hit /up and Rails::HealthController renders a green page with a 200, or a red one with a 500 if something raised on the way in. You do not have to add it, wire it or name it. Most people running Rails in production already have this endpoint and have never opened it, which is worth knowing before you go shopping for a gem.

The next thing to know is what it does not do, and Rails is honest about this in the controller's own documentation: the endpoint does not reflect the status of your application's dependencies. It answers exactly one question. Did this process boot without raising. A Puma worker whose connection pool is full of dead Postgres sockets booted fine hours ago, and it will keep answering /up with a cheerful 200 while every real request 500s. That is the failure mode that costs you the outage. A monitor pointed at /up sits green through the whole thing, and a monitor you have learned to trust while it is wrong is worse than no monitor at all.

The controller you write when the database matters

app/controllers/health_controller.rb
class HealthController < ApplicationController
  # Whatever ApplicationController runs before every action, skip it here.
  skip_before_action :authenticate_user!, raise: false

  def show
    ActiveRecord::Base.with_connection { |c| c.select_value("SELECT 1") }
    render json: { ok: true }
  rescue StandardError => e
    Rails.logger.warn("health check failed: #{e.class}")
    # 503, so a booted app with a dead database reads as down.
    render json: { ok: false, error: "database" }, status: :service_unavailable
  end
end

Then point the existing route at it in config/routes.rb: get "up" => "health#show", as: :rails_health_check. Keeping the path means the load balancer config, the Kubernetes probe and anything else already hitting /up carry on working without a change.

What to check and what to leave out

  • One query against the database you cannot serve a request without. SELECT 1 through the pool is enough: it proves the pool still hands out a live connection, and it costs well under a millisecond.
  • Redis, only if a request genuinely fails without it. If the app degrades to a slower page, that is not something worth waking anyone up for.
  • Nothing that calls a third-party API. Fan out to Stripe and S3 and you will page yourself at 3am for someone else's outage, about a thing you cannot fix.
  • A budget well under a second. The Logdash pinger gives up after 10 seconds and records the check as down, and an endpoint that needs 10 seconds is already telling you something.
  • No version string, no migration status, no environment dump in the body. Assume the URL is public, because eventually it is.

Point a monitor at it

  1. 1
    Ship it, then try to break it Deploy, open /up in a browser, then stop Postgres on your machine and load the same path. You want a small JSON body with a 503, not a 500 page with a stack trace in it.
  2. 2
    Create the monitor Add the service in Logdash and paste the full URL. Checks run every 5 minutes on the free plan, every minute on Builder and every 15 seconds on Pro, and each one records the status code and the response time.
  3. 3
    Take the database away Point staging at a dead database and wait one interval. The status code leaves the 200-399 range, the monitor flips to down on that transition, and the Telegram alert arrives naming the endpoint and the code it got back.

The one that catches people: host authorization

If you have set config.hosts in production, ActionDispatch::HostAuthorization answers any request with an unexpected Host header with 403 Forbidden. An external monitor hitting your real domain is fine. A load balancer or a Kubernetes probe hitting the pod by IP is not, and 403 sits outside 200-399, so the check reads as down while the app is perfectly healthy. The Rails guide gives you the exclusion: config.host_authorization = { exclude: ->(request) { request.path == "/up" } }. Set it before you lose an evening to a red monitor pointed at a green app.

Does Rails 8 have a built-in health check endpoint?

Yes. Every generated app routes /up to Rails::HealthController, which returns 200 if the app booted without raising and 500 if it did not. It has been the default since Rails 7.1, so most apps already have it and most owners have never looked.

What does the Rails /up endpoint actually check?

That the process is running and boots clean. Nothing else. It never touches the database, Redis or a queue, and the controller documentation says so directly, which is why a custom action is the answer as soon as the database matters.

What status code should a Rails health check return?

200 when the app can serve traffic, 503 when a dependency it needs is gone. Logdash marks a monitor down on anything outside 200-399, so the 503 is what turns the endpoint into an alert instead of a chart.

Do I need a health check gem for Rails?

Usually not. health_check and okcomputer earn their place once you want a registry of named checks across a large app. For one controller and one SELECT 1, a gem is more configuration than code.

Should a Rails health check endpoint be authenticated?

Leave it open and keep the body boring. Monitors and load balancers cannot carry credentials easily, and a response of ok true leaks nothing. If it truly has to be private, restrict it by network rather than by password.

Point it at your own URL and watch it for real.

Any public URL · checked every 15 s