Introduction
Deploying a Laravel application is only half the battle. Once it's live, you need to know immediately if something breaks—before your customers do. Health checks and uptime monitoring are the safety net that keeps your team confident your app is serving traffic reliably. In this guide, we'll walk through health checks and uptime for Laravel: a practical guide covering built-in Laravel 11+ features, dependency checks, queue monitoring, and external uptime services that will give you visibility into your application's real-world health.
Built-in /up route — Laravel 11+ includes a health endpoint
Laravel 11 introduced a native health check endpoint at /up, making it trivial to verify your application is running. This route returns a 200 response with minimal overhead when the app boots successfully.
The endpoint is defined in routes/web.php by default:
Route::get('/up', function () {
return response()->json(['status' => 'up']);
});You can customize the response or add logic before it returns. For example, if you want to fail the health check when maintenance mode is active:
Route::get('/up', function () {
if (app()->maintenanceMode()->active()) {
return response()->json(['status' => 'maintenance'], 503);
}
return response()->json([
'status' => 'up',
'timestamp' => now()->toIso8601String(),
]);
});In production, this endpoint should never require authentication. It's the first signal to monitoring systems (and load balancers) that your application is alive. Keep the response lean and the logic minimal so it doesn't become a bottleneck itself.
Check dependencies — Database, cache and queue connectivity
A 200 response from /up means the app booted, not that it's fully functional. Your database might be down, Redis might be unreachable, or your queue might be misconfigured. A comprehensive health check validates your critical dependencies.
Create a dedicated health check controller to encapsulate this logic:
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class HealthCheckController extends Controller
{
public function __invoke()
{
$checks = [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
'redis' => $this->checkRedis(),
];
$allHealthy = collect($checks)->every(fn($status) => $status === 'ok');
return response()->json(
['status' => $allHealthy ? 'healthy' : 'degraded', 'checks' => $checks],
$allHealthy ? 200 : 503
);
}
private function checkDatabase(): string
{
try {
DB::connection()->getPdo();
return 'ok';
} catch (\Exception $e) {
return 'failed: ' . $e->getMessage();
}
}
private function checkCache(): string
{
try {
Cache::put('health_check', true, 1);
return Cache::pull('health_check') ? 'ok' : 'failed: not retrieved';
} catch (\Exception $e) {
return 'failed: ' . $e->getMessage();
}
}
private function checkRedis(): string
{
try {
Redis::ping();
return 'ok';
} catch (\Exception $e) {
return 'failed: ' . $e->getMessage();
}
}
}Wire it into your routes:
Route::get('/health', HealthCheckController::class);Now, /health returns a 503 if any dependency fails, and your monitoring system can act accordingly—alerting the team or failing over to a backup instance. This endpoint should also bypass authentication and rate limiting.
Queue health — Alert when job wait times grow
Queued jobs that pile up unprocessed are a silent killer. Your app appears healthy, but background work grinds to a halt. Monitoring queue depth and processing times reveals this problem early.
Extend your health check to include queue metrics:
private function checkQueueHealth(): array
{
$queues = config('queue.default') === 'redis'
? ['default', 'high', 'low']
: ['default'];
$health = [];
foreach ($queues as $queue) {
try {
$size = Redis::llen("queues:{$queue}");
$threshold = 1000; // Alert if > 1000 jobs pending
$health[$queue] = [
'pending' => $size,
'status' => $size > $threshold ? 'warning' : 'ok',
];
} catch (\Exception $e) {
$health[$queue] = ['status' => 'failed', 'error' => $e->getMessage()];
}
}
return $health;
}Add this to your health check response and also consider a separate endpoint for queue diagnostics:
Route::get('/health/queue', function () {
return response()->json([
'pending_jobs' => Redis::llen('queues:default'),
'failed_jobs' => DB::table('failed_jobs')->count(),
'workers_active' => Redis::hlen('queues:workers'),
]);
});In production, set up alerts in your monitoring tool (DataDog, New Relic, etc.) to fire when queue depth exceeds thresholds or when workers haven't picked up a job in X minutes. This gives you a 15-minute warning before users start complaining about delayed emails or notifications.
External monitoring — Uptime checks from outside your network
Internal health checks tell you if your app thinks it's healthy. External monitoring tells you if customers can actually reach it. These are different problems. A firewall issue, DNS misconfiguration, or certificate expiry won't show up in your /health endpoint.
Use a dedicated uptime monitoring service like Uptime Robot, Pingdom, Datadog, or New Relic to ping your health endpoint from multiple geographic locations every 1–5 minutes. When an external check fails, the team knows there's a routing or infrastructure issue, not just an app problem.
Configure your monitoring service with:
- URL:
https://your-app.com/health - Method:
GET - Acceptable status codes:
200–299 - Check interval: 5 minutes (or 1 minute for critical services)
- Alert contacts: PagerDuty, Slack, email
- Locations: US East, US West, Europe, Asia (or your key regions)
Also add a synthetic transaction monitor that performs a real workflow—logging in, creating a resource, querying data—rather than just hitting a status endpoint. This catches subtle bugs that a simple health check misses.
For sensitive endpoints or those behind authentication, create a dedicated monitoring user or API key that the uptime service uses. Log these calls separately so you can distinguish them from real user traffic.
Common mistakes
- Over-checking dependencies: Pinging every microservice in your health check inflates response time and creates cascading failures. Prioritize: database and cache are critical; external APIs are not.
- Missing authentication bypass: If your health endpoint requires login, monitoring systems can't reach it. Always exclude
/healthand/upfrom middleware that blocks unauthenticated requests. - No alerting strategy: Monitoring without alerting is noise. Set thresholds, define who gets paged, and automate responses (e.g., auto-restart workers, trigger failover).
- Ignoring slow checks: A health endpoint that takes 5 seconds defeats its purpose and will timeout under load. Keep checks under 500ms.
- Not testing the endpoint: Deploy, then actually verify
curl https://your-app.com/healthreturns what you expect before rolling out to all servers.
Key takeaways
- Use Laravel 11's
/upendpoint as your baseline. It's fast and requires zero setup. - Build a more detailed
/healthendpoint that checks database, cache, Redis, and queue connectivity. Return 503 if anything fails. - Monitor queue depth and job processing times to catch bottlenecks before users notice delays.
- Pair internal health checks with external uptime monitoring from multiple regions. These catch different failure modes.
- Exclude health endpoints from authentication, rate limiting, and middleware that could block legitimate monitoring requests.
- Set clear alert thresholds and ensure the team knows who responds to page-outs at 3 a.m.






Comments
Loading comments…