DEV Community

Vigilmon
Vigilmon

Posted on

How to Monitor Your PHP/Laravel App Uptime (and Get Notified When It Goes Down)

How to Monitor Your PHP/Laravel App Uptime (and Get Notified When It Goes Down)

Your Laravel app went down at 2 AM. You found out at 9 AM when a customer emailed you. Sound familiar?

Most Laravel tutorials end at deployment. This one picks up there. By the end you'll have HTTP uptime monitoring, heartbeat checks for your queued jobs and Artisan commands, Slack alerts, a public status page, and a live status badge for your README — all in under 30 minutes, free.


The two failures PHP devs miss most

Laravel production failures fall into two patterns that are easy to detect but go unnoticed without the right tooling:

Endpoint outages — your app starts returning 500s or timing out. Users hit a broken page. You don't find out until they complain (or they don't complain and just leave).

Silent queue failures — your queue workers stop processing jobs. Emails stop sending. Webhooks stop being delivered. Reports stop generating. No exception is thrown. Nothing is logged. You just quietly stop getting results until something downstream breaks.

Both are solvable with a health endpoint and a monitoring tool. Let's wire them up.


Step 1: Add a health check route

Laravel doesn't ship with a health endpoint out of the box, but adding one takes about two minutes.

Install the spatie/laravel-health package:

composer require spatie/laravel-health
Enter fullscreen mode Exit fullscreen mode

Publish and run the migrations:

php artisan vendor:publish --tag="health-migrations"
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Register your checks in AppServiceProvider:

// app/Providers/AppServiceProvider.php
use Spatie\Health\Facades\Health;
use Spatie\Health\Checks\Checks\DatabaseCheck;
use Spatie\Health\Checks\Checks\CacheCheck;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck;

public function boot(): void
{
    Health::checks([
        DatabaseCheck::new(),
        CacheCheck::new(),
        UsedDiskSpaceCheck::new()->failWhenUsedSpaceIsAbovePercentage(90),
    ]);
}
Enter fullscreen mode Exit fullscreen mode

The package automatically registers a /up route. You can verify it locally:

php artisan serve
curl http://localhost:8000/up
# {"finishedAt":"...","checkResults":[...]}
Enter fullscreen mode Exit fullscreen mode

A 200 means healthy. A 500 means something is wrong — and it tells you exactly what.

If you prefer a minimal, zero-dependency health check, you can wire one up yourself:

// routes/web.php
Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        return response()->json(['status' => 'ok']);
    } catch (\Exception $e) {
        return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
    }
});
Enter fullscreen mode Exit fullscreen mode

Either approach works. The important thing is that you have a URL that returns a non-200 when your app is in trouble.


Step 2: Set up HTTP uptime monitoring

With your health endpoint live, point Vigilmon at it:

  1. Sign up for a free account at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/up (or /health)
  4. Set check interval to 5 minutes (free tier)
  5. Save

Vigilmon will ping your endpoint every 5 minutes from multiple locations. If it returns a non-200 or times out, you'll get alerted immediately.

You can also add separate monitors for critical routes:

  • https://yourdomain.com/ — your homepage is serving
  • https://yourdomain.com/api/ping — your API layer is responding
  • https://yourdomain.com/login — your auth flow is reachable

Each monitor runs independently. If your API goes down but the homepage is fine, you'll know exactly which layer is broken.


Step 3: Heartbeat monitoring for queued jobs

HTTP monitoring catches server outages. But what about jobs that silently stop running?

The heartbeat pattern: your job pings a URL at the end of every successful run. If Vigilmon stops receiving pings within the expected interval, it alerts you — whether the job failed, threw an exception, or stopped being dispatched altogether.

Here's a Laravel job with a heartbeat:

// app/Jobs/SendDailyDigestJob.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Http;

class SendDailyDigestJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function handle(): void
    {
        // Your actual job logic
        $this->sendDigestEmails();

        // Ping the heartbeat URL on success
        $heartbeatUrl = config('services.vigilmon.digest_heartbeat_url');
        if ($heartbeatUrl) {
            Http::timeout(5)->get($heartbeatUrl);
        }
    }

    private function sendDigestEmails(): void
    {
        // your logic here
    }
}
Enter fullscreen mode Exit fullscreen mode

Add the URL to your config:

// config/services.php
'vigilmon' => [
    'digest_heartbeat_url' => env('VIGILMON_DIGEST_HEARTBEAT_URL'),
],
Enter fullscreen mode Exit fullscreen mode
# .env
VIGILMON_DIGEST_HEARTBEAT_URL=https://vigilmon.online/heartbeats/your-unique-token
Enter fullscreen mode Exit fullscreen mode

In Vigilmon, create a Heartbeat Monitor:

  1. Click New Monitor → Heartbeat
  2. Set the expected interval (e.g. every 24 hours)
  3. Copy the unique ping URL
  4. Set it as VIGILMON_DIGEST_HEARTBEAT_URL in your environment

Works with Artisan commands too

If you're running scheduled tasks via artisan schedule:run, the same pattern applies:

// app/Console/Commands/GenerateReportsCommand.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class GenerateReportsCommand extends Command
{
    protected $signature = 'reports:generate';
    protected $description = 'Generate daily reports';

    public function handle(): int
    {
        $this->generateReports();

        // Ping heartbeat on success
        $url = config('services.vigilmon.reports_heartbeat_url');
        if ($url) {
            Http::timeout(5)->get($url);
        }

        return Command::SUCCESS;
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in your scheduler:

// app/Console/Kernel.php (or routes/console.php in Laravel 11+)
Schedule::command('reports:generate')->dailyAt('06:00');
Enter fullscreen mode Exit fullscreen mode

The heartbeat monitors the actual execution — not just whether the cron fires. If the command crashes halfway through, no ping is sent and you get alerted.


Step 4: Alerts via Slack or email

Configure how you want to be notified in Vigilmon:

For Slack:

  1. Create an incoming webhook in your Slack workspace
  2. In Vigilmon, go to Notifications → New Channel → Slack
  3. Paste the webhook URL
  4. Enable it on each monitor

For email:

  1. In Vigilmon, go to Notifications → New Channel → Email
  2. Enter your address or a distribution list
  3. Enable it on your monitors

Alert messages look like:

🔴 DOWN: yourdomain.com/up
Status: 500 Internal Server Error
Detected from: US-East, EU-West
3 minutes ago
Enter fullscreen mode Exit fullscreen mode

Recovery notification:

✅ RECOVERED: yourdomain.com/up is back UP
Downtime: 14 minutes
Enter fullscreen mode Exit fullscreen mode

Heartbeat alert:

🔴 MISSED: Daily Digest Job heartbeat
Expected every: 24 hours
Last ping received: 26 hours ago
Enter fullscreen mode Exit fullscreen mode

Step 5: Public status page and README badge

A public status page answers the "is it just me?" question for your users — and reduces support noise during incidents.

In Vigilmon:

  1. Go to Status Pages → New Status Page
  2. Name it and select which monitors to display
  3. Copy the public URL and share it in your docs, footer, or error pages

You can also embed a live status badge in your GitHub README. Every monitor on Vigilmon has a badge endpoint at:

https://vigilmon.online/badge/{your-monitor-slug}
Enter fullscreen mode Exit fullscreen mode

The badge shows current status (up, down, or degraded) and response time. Add it to your README.md:

![Uptime](https://vigilmon.online/badge/your-monitor-slug)
Enter fullscreen mode Exit fullscreen mode

Or as an HTML embed that links to your status page:

<a href="https://status.yourdomain.com">
  <img src="https://vigilmon.online/badge/your-monitor-slug" alt="Uptime status">
</a>
Enter fullscreen mode Exit fullscreen mode

What you've built

What How
HTTP uptime monitoring /up route + Vigilmon HTTP monitor
Database / cache health spatie/laravel-health checks
Queue job monitoring Heartbeat ping at end of each job
Artisan command monitoring Heartbeat ping on successful completion
Instant alerts Slack or email webhook notifications
Public status page Vigilmon status page
README status badge /badge/{slug} SVG embed

The full setup costs $0 on Vigilmon's free tier and takes less time than tracking down a silent queue failure that's been running for two days.


Next steps

  • Add a heartbeat for every critical background job, not just your daily ones
  • Monitor response time trends — a gradual slowdown is often detectable hours before a full outage
  • Add UsedDiskSpaceCheck and RedisCheck to your health checks so disk-full events and cache failures surface before they cause downtime

Get started free at vigilmon.online.

Top comments (0)