Laravel health check endpoint and the built-in /up route

Laravel 11 and 12 register a health route at /up out of the box, but it only proves the framework booted, so add a route that runs one database query and returns 503 when it fails.

Open bootstrap/app.php in any Laravel 11 or 12 app and the health route is already there, passed as an argument: Application::configure()->withRouting(web: ..., commands: ..., health: '/up'). Laravel registers a GET route on that path, fires a DiagnosingHealth event and returns 200 unless a listener throws, in which case you get a 500. Nothing subscribes to that event in a fresh app, so out of the box /up answers 200 whenever PHP-FPM is alive and the framework boots.

So the useful question is not whether you have a health endpoint. It is whether yours can fail. A booted Laravel app that can no longer reach MySQL still returns 200 from /up while every request to a real controller throws a QueryException, and the uptime monitor watching the site records a clean day through the incident. The check has to touch the thing the app needs: one cheap query, and a status code outside the success range when that query does not come back.

A route that fails when the database does

routes/web.php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;

Route::get('/health', function () {
    try {
        DB::select('select 1');
    } catch (\Throwable $e) {
        report($e);

        // 503, so a booted app with a dead database reads as down.
        return response()->json(['ok' => false, 'error' => 'database'], 503);
    }

    return response()->json(['ok' => true]);
});

If you would rather keep the built-in path, hook the event instead. In AppServiceProvider::boot, Event::listen(DiagnosingHealth::class, fn () => DB::select('select 1')). A throw from that listener turns /up into a 500, and a monitor reads a 500 the same way it reads a 503.

What belongs in the check

  • The default connection, once, with something as cheap as select 1. DB::select uses the same pool and the same credentials your controllers use, so it fails for the same reasons they do.
  • Redis or the queue, only when the app cannot serve a page without them. A slower cache miss is not an outage.
  • Nothing that reaches a payment provider or an object store. That turns their bad night into your pager, and their night is not yours to fix.
  • Under a second, end to end. The Logdash pinger times out at 10 seconds and records the check as down, so a slow health route becomes a false alarm.
  • A body of two keys. No app version, no queue depth, no config values. Assume anyone can curl the URL, because they can.

Point a monitor at it

  1. 1
    Check both directions Load /health in a browser, then break the database credentials in a staging .env and load it again. You want your JSON with a 503, so confirm APP_DEBUG is false anywhere a monitor can reach.
  2. 2
    Create the monitor Add the service in Logdash, paste the URL, and the first check runs straight away. Every check stores the status code and the response time, at 5-minute intervals on the free plan, 1 minute on Builder and 15 seconds on Pro.
  3. 3
    Prove the alert Stop the database container and wait one interval. The status code leaves the 200-399 window, the monitor flips to down on the transition, and Telegram gets a message naming the endpoint and the code.

artisan down does not take /up down

Laravel excludes the built-in health path from maintenance mode on purpose: withRouting registers it through PreventRequestsDuringMaintenance::except(). Run php artisan down and every page returns 503 while /up carries on returning 200. That is right for a load balancer, which should keep routing to a pod that is deliberately in maintenance, and wrong for an uptime monitor, which will report the site as fine while nobody can buy anything. A custom route in routes/web.php goes through that middleware and returns 503 like everything else, so the monitor follows your deploys. Decide which of the two you want before you pick the URL.

Does Laravel have a built-in health check route?

Yes, since Laravel 11. bootstrap/app.php passes health: '/up' to withRouting, which registers a GET route that fires the DiagnosingHealth event and returns 200 unless a listener throws. Laravel 12 ships the same line.

What is the health check URL in Laravel?

/up by default. The path is whatever string you pass to health: in bootstrap/app.php, so health: '/healthz' moves it. A route of your own can live anywhere, and /health and /healthz are the two conventions worth picking between.

Is there a Laravel health check package?

spatie/laravel-health is the well-known one and it earns its keep once you have a dozen named checks with thresholds and a dashboard. For one query and one status code, a closure in routes/web.php is less to maintain.

Is there an artisan command for health checks?

Not in the framework. php artisan about prints environment and cache state locally, and spatie/laravel-health adds health:check for cron or CI. Neither is what a monitor should use, because a monitor needs an HTTP status code.

What status code should a Laravel health check return?

200 when the app can serve, 503 when it cannot. Logdash marks a monitor down on anything outside 200-399, so the status code is the difference between a chart nobody reads and an alert that arrives.

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

Any public URL · checked every 15 s