Back to Blog
Tutorials April 9, 2026 · 8 min read

How to Monitor a Laravel API in Production: A Complete Setup Guide

Laravel apps in production need more than error logs. Here is a complete monitoring setup covering health checks, uptime monitoring, and alerting.

Getting a Laravel API into production is one thing. Knowing that it is actually working — that the database is responding, queues are processing, and endpoints are returning correct data — is another matter entirely.

This guide covers a complete monitoring setup for a Laravel production API: how to build a proper health check endpoint, how to configure external uptime monitoring, what to do about queue workers and scheduled tasks, and how to make sure the right people get notified when something breaks.


Part 1: Build a Health Check Endpoint

The foundation of any good monitoring setup is a dedicated health check endpoint. Rather than monitoring your business-logic endpoints directly (which creates noise every time you update a response format), a health check endpoint is a stable, purpose-built signal of whether your application is healthy.

A Basic /health Endpoint

Add a route to routes/api.php (or routes/web.php if you prefer):

Route::get('/health', [HealthController::class, 'check']);

And a controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;

class HealthController extends Controller
{
    public function check(): JsonResponse
    {
        $checks = [];
        $healthy = true;

        // Database check
        try {
            DB::connection()->getPdo();
            $checks['database'] = 'ok';
        } catch (\Exception $e) {
            $checks['database'] = 'error';
            $healthy = false;
        }

        // Cache check
        try {
            Cache::put('health_check', true, 5);
            Cache::get('health_check');
            $checks['cache'] = 'ok';
        } catch (\Exception $e) {
            $checks['cache'] = 'error';
            $healthy = false;
        }

        // Redis check (if using Redis separately from cache)
        try {
            Redis::ping();
            $checks['redis'] = 'ok';
        } catch (\Exception $e) {
            $checks['redis'] = 'error';
            $healthy = false;
        }

        $status = $healthy ? 200 : 503;

        return response()->json([
            'status'  => $healthy ? 'ok' : 'degraded',
            'checks'  => $checks,
            'version' => config('app.version', '1.0.0'),
            'time'    => now()->toIso8601String(),
        ], $status);
    }
}

This endpoint returns 200 with {"status": "ok"} when everything is healthy, and 503 with {"status": "degraded"} when any dependency is unreachable. The monitoring tool checks the status code and optionally validates that the response body contains "status":"ok".

What to Check in Your Health Endpoint

For most Laravel APIs, the useful checks are:

Database connectivity — Can you get a PDO connection? This catches database server outages, credential rotation issues, and connection pool exhaustion.

Cache connectivity — Can you write to and read from the cache? If you are using Redis as your cache backend, this also covers basic Redis availability.

Queue connectivity — If your application depends heavily on queued jobs, a health check that verifies the queue connection is reachable adds meaningful signal.

External dependency connectivity — If your application cannot function without a critical third-party service, add a lightweight check (ping, not a full API call) to surface issues with that dependency.

Avoid making the health check endpoint too heavy. It should be fast (<100ms) and not trigger any side effects. Do not run actual queries that hit the database beyond a simple connection check.

Exclude the Health Endpoint from Rate Limiting

Your monitoring tool calls /health every minute (or faster). Make sure the route is excluded from your API rate limiting middleware:

// routes/api.php
Route::get('/health', [HealthController::class, 'check'])
    ->withoutMiddleware(['throttle:api']);

Part 2: Set Up External Uptime Monitoring

A health check endpoint is only useful if something external is calling it regularly and alerting you when it stops responding correctly. That is what an external uptime monitoring service provides.

What to Monitor

At minimum, monitor:

  1. /health — Your dedicated health check endpoint. This is your primary signal for infrastructure-level failures.

  2. Your authentication endpointPOST /api/v1/login or equivalent. Authentication failures affect every user. A quick check that the endpoint accepts a test request and returns the expected response structure catches a wide range of breakage.

  3. One or two core business endpoints — The endpoints your users depend on most. These do not need deep validation — just a check that they return 200 and take under 2 seconds.

Configure Your PulseAPI Monitor

For each of these endpoints, create a monitor in PulseAPI with:

URL: Your full endpoint URL (https://api.yourapp.com/health)

Method: GET (for health checks), or POST with a test body for auth endpoints

Success criteria:

  • Status code: 200–299
  • Response body contains: "status":"ok" (for the health endpoint)
  • Response time threshold: 2000ms (adjust based on your actual p95)

Check interval: 1 minute

Consecutive failures before incident: 2 (eliminates false positives from transient network errors)

Notification: Email + Slack

Multi-region monitoring is on by default — your endpoint will be checked from US East and US West simultaneously. If both regions report failure, confidence in the incident is high. If only one region fails, it may indicate a routing issue rather than a server problem.


Part 3: Monitor Your Queue Workers

Laravel Horizon (if you are using it) has a built-in dashboard at /horizon that shows queue throughput, job failure rates, and worker status. This is useful for manual review but does not alert you when things go wrong.

For production queue monitoring, use a combination of:

Horizon Metrics in Your Health Check

Extend your health check to include basic queue status:

// Add to HealthController::check()
try {
    $horizonStatus = \Illuminate\Support\Facades\Redis::hget(
        'horizon:master-supervisor',
        'status'
    );
    $checks['queue'] = ($horizonStatus === 'running') ? 'ok' : 'paused';
    if ($horizonStatus !== 'running') {
        $healthy = false;
    }
} catch (\Exception $e) {
    $checks['queue'] = 'unknown';
}

Alert on Failed Jobs

Laravel has a job.failed event and a failed_jobs database table. Add a notification when a job fails more than N times in a period:

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\Events\JobFailed;

Queue::failing(function (JobFailed $event) {
    // Log to your error tracker (Sentry, Bugsnag, etc.)
    // Or send a Slack notification for critical job classes
    if (in_array(get_class($event->job), config('monitoring.critical_jobs'))) {
        // Alert immediately
    }
});

Horizon Heartbeat (for Long-Running Workers)

If your Horizon workers stop processing (without crashing, which would be easy to detect), you want to know. One approach is a "heartbeat" job that runs on a schedule and updates a timestamp in Redis. If the timestamp becomes stale, your health check catches it:

// A simple heartbeat job
class QueueHeartbeatJob implements ShouldQueue
{
    public function handle(): void
    {
        Cache::put('queue_heartbeat', now()->timestamp, 120); // 2-minute TTL
    }
}

// In HealthController, check the heartbeat
$lastBeat = Cache::get('queue_heartbeat');
$checks['queue_heartbeat'] = ($lastBeat && now()->timestamp - $lastBeat < 90) ? 'ok' : 'stale';

Schedule this job to run every minute in routes/console.php:

Schedule::job(new QueueHeartbeatJob)->everyMinute();

Part 4: Monitor Scheduled Tasks

Laravel's scheduler runs via a cron entry that calls php artisan schedule:run every minute. If the cron entry disappears, is misconfigured, or the schedule:run command fails silently, your scheduled tasks stop running — and you may not notice for days.

Add a Scheduler Heartbeat

This is the same pattern as the queue heartbeat, but specifically for the scheduler. Add a scheduled task that updates a cache key every minute:

// routes/console.php
Schedule::call(function () {
    Cache::put('scheduler_heartbeat', now()->timestamp, 120);
})->everyMinute()->name('scheduler-heartbeat')->withoutOverlapping();

Include this in your health check:

$lastSchedulerBeat = Cache::get('scheduler_heartbeat');
$checks['scheduler'] = ($lastSchedulerBeat && now()->timestamp - $lastSchedulerBeat < 90)
    ? 'ok'
    : 'stale';
if ($checks['scheduler'] === 'stale') {
    $healthy = false;
}

Now if the cron stops running, your health check returns 503 within 2 minutes, and your uptime monitor fires an alert.

Use a Dead Man's Switch Service

For critical scheduled tasks, consider a dedicated service like Healthchecks.io or Oh Dear's cron monitoring. These services provide a URL that your scheduled task pings on successful completion. If the ping is not received within a configured window, an alert fires.

Schedule::command('report:generate-daily')
    ->dailyAt('03:00')
    ->thenPing('https://hc-ping.com/your-uuid-here');

This is a more reliable signal than a heartbeat because it confirms the task actually completed rather than just that the scheduler is running.


Part 5: Error Tracking with Sentry

External uptime monitoring tells you when an endpoint is unavailable or slow. Error tracking tells you about application-level errors that do not cause downtime — 500 responses, unhandled exceptions, validation errors at unexpected rates.

Install the Laravel Sentry package:

composer require sentry/sentry-laravel
php artisan sentry:publish --dsn=your_dsn_here

Configure config/sentry.php to capture the information that is useful for debugging:

return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.1), // 10% of transactions
    'profiles_sample_rate' => 0,
    'send_default_pii' => false, // Don't capture user PII by default
];

At 10% traces sample rate, you capture meaningful performance data without overwhelming your Sentry quota. Adjust based on your traffic volume and Sentry plan.


Your Complete Production Monitoring Checklist

After completing this setup, you should have:

  • [ ] /health endpoint returning 200 + {"status":"ok"} when all dependencies are healthy, 503 when any are not
  • [ ] PulseAPI monitors running every minute from US East and US West against /health, your auth endpoint, and your core business endpoints
  • [ ] Alert notifications configured to your email and Slack channel
  • [ ] Queue heartbeat job scheduled every minute, checked by health endpoint
  • [ ] Scheduler heartbeat call scheduled every minute, checked by health endpoint
  • [ ] Sentry installed and capturing application exceptions
  • [ ] Maintenance windows configured for your deployment schedule

This setup takes 2–3 hours to complete. It catches the vast majority of production failure modes — infrastructure outages, queue stalls, scheduler failures, application exceptions, and response time degradation — before customers report them.


PulseAPI monitors your Laravel API from multiple regions with 1-minute check intervals and smart alert routing. Start free →

Ready to Monitor Your APIs Intelligently?

Join developers running production APIs. Free for up to 10 endpoints.

Start Monitoring Free

No credit card  ·  10 free endpoints  ·  Cancel anytime