Laravel 11 health checks: what a green /up leaves untested

Trace the health event through a real Laravel HTTP kernel, then expose two defects that report success while a synthetic dependency is broken.
Start with the failure you need to detect
A 200 from /up is useful only if you know what ran before that response. In a small Laravel 11.0.0 fixture, the default route returns 200 without calling a synthetic dependency that would throw. Registering that dependency as a health listener changes the result to 500. Removing the listener makes the endpoint green again while leaving the dependency broken.
This walkthrough is for an engineer maintaining an older application and checking its upgrade assumptions. It uses the initial Laravel 11.0.0 release under PHP 8.3.35. Laravel 11's security support ended on 12 March 2026, and the fixture audit records six advisories affecting its deliberately historical framework version. Use the bundle to reproduce the old behavior, not to start a production application.
Keep the structure change separate
Laravel 11 introduced a smaller skeleton for new applications, with routing, middleware and exception configuration concentrated in bootstrap/app.php. The upgrade guide recommends retaining the Laravel 10 application structure when upgrading an existing project to 11. Moving files is not required to establish framework compatibility.
Our fixture's app.php registers only the health route. Its companion Docker recipe creates the config and bootstrap directories, uses array-backed session and cache settings, and disables debug output. It is a minimal test application, not a replacement for a production bootstrap file. Preserve your existing routes, middleware and exception handling when applying the relevant change.
<?php
use Illuminate\Foundation\Application;
return Application::configure(basePath: __DIR__)
->withRouting(health: '/up')
->withMiddleware()
->withExceptions()
->create();
Follow the event to the dependency
In the pinned framework source, the health callback dispatches DiagnosingHealth before rendering its successful response. The helper registers a listener that calls the supplied probe. That call is the boundary under test: a successful probe lets the route continue, while a thrown exception reaches the HTTP error handler.
The probe is a closure with a counter and an optional synthetic exception. It performs no SQL, DNS lookup or network request. The test invokes the actual Laravel HTTP kernel with a Request object. It does not send traffic through Nginx, a load balancer or a TLS listener.
Default registration produces 200 and zero dependency calls. The healthy listener produces 200 and one call. The failing listener produces 500 and one call. The last result is useful evidence because the test checks both the response and whether the dependency was invoked. Checking only that the application can construct a response would miss an unwired listener.
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Events\DiagnosingHealth;
function registerHealthCheck(Application $app, Closure $probe): void
{
$app['events']->listen(DiagnosingHealth::class, static function () use ($probe): void {
$probe();
});
}
Make false green results fail the test
The same failure assertion is run against two altered copies of the helper. One returns before registering the listener. The other catches and discards the dependency exception. Both return 200 when the test expects 500, producing the intended status failure. The first makes zero dependency calls; the second makes one. Those observations distinguish absent wiring from suppressed errors.
With debug disabled, the synthetic diagnostic is absent from the failing public response. This checks one controlled exception. It does not prove that every custom exception handler or reverse proxy conceals diagnostics. Keep operator detail in appropriate internal logs and verify the actual public monitoring path during deployment.
Decide what should happen after a failed check
The event hook lets you add checks, but it cannot choose the operational response for you. For a shared database outage, restarting every web process does not repair the database. I recommend documenting separately which failures should stop routing new work, trigger a process restart, or alert an operator. Include a dependency timeout in that design; this fixture throws immediately and does not prove timeout behavior.
A queue worker or WebSocket delivery path needs its own evidence. No Reverb process, broadcast worker, channel authorization or reconnect path runs in this example, so /up says nothing about their end-to-end behavior. A framework upgrade and adding real-time delivery are separate changes to verify.
Before accepting an upgrade, preserve this failure case in the existing application's tests, verify that the listener is registered through its normal startup path, and exercise the deployed monitor URL. The fixture establishes the event/response mechanism; the application and deployment checks establish whether your service actually uses it.
Reproduce the examples
Exact bootstrap and listener, kernel request tests, two deliberate defects, pinned dependency lock, runtime recipe and recorded advisory report. Requires Docker and Python 3. Historical unsupported Laravel release; no production deployment.
Download the source and tests (.zip)
