tutorial

Uptime monitoring for Symfony applications

Your Symfony application is live. Users are hitting it. But is it actually up right now? Is your Symfony Messenger worker still processing jobs? Do you know ...

Your Symfony application is live. Users are hitting it. But is it actually up right now? Is your Symfony Messenger worker still processing jobs? Do you know the moment something breaks at 3am?

This tutorial walks through adding production-grade uptime monitoring to a Symfony application using Vigilmon. We will cover:

  • A health check endpoint your monitoring service can ping
  • Webhook alerts to notify you instantly when the app goes down
  • A heartbeat monitor to detect when Symfony Messenger workers silently die

By the end you will have a Symfony app that actively reports its own health, rather than waiting for a user complaint to discover downtime.


Prerequisites

  • PHP 8.1+ with Symfony 6.x or 7.x
  • Composer
  • A free account at vigilmon.online

Part 1: Add a health check endpoint

The simplest form of uptime monitoring is HTTP — Vigilmon pings your URL every minute and alerts you if it doesn't get a 200 response.

First, create a dedicated health check controller:

<?php
// src/Controller/HealthController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Doctrine\DBAL\Connection;

class HealthController
{
    public function __construct(
        private readonly Connection $connection,
    ) {}

    #[Route('/health', name: 'app_health', methods: ['GET'])]
    public function __invoke(): JsonResponse
    {
        $checks = [];
        $status = 'ok';

        // Database connectivity check
        try {
            $this->connection->executeQuery('SELECT 1');
            $checks['database'] = 'ok';
        } catch (\Throwable $e) {
            $checks['database'] = 'error: ' . $e->getMessage();
            $status = 'degraded';
        }

        $httpStatus = $status === 'ok' ? 200 : 503;

        return new JsonResponse([
            'status'    => $status,
            'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
            'checks'    => $checks,
        ], $httpStatus);
    }
}

Register the route in config/routes.yaml if you are not using attribute routing:

app_health:
    path: /health
    controller: App\Controller\HealthController
    methods: [GET]

Test it locally:

curl http://localhost:8000/health

Expected output:

{
  "status": "ok",
  "timestamp": "2026-06-29T07:00:00+00:00",
  "checks": {
    "database": "ok"
  }
}

If the database is unreachable the endpoint returns HTTP 503, which Vigilmon will treat as a failure and trigger an alert.

Excluding the health endpoint from authentication

If your application uses a firewall or authentication middleware, exclude /health so Vigilmon's monitoring requests are not redirected to a login page:

# config/packages/security.yaml
security:
    firewalls:
        health:
            pattern: ^/health
            security: false
        main:
            # ... your existing firewall config

Part 2: Set up HTTP monitoring in Vigilmon

  1. Log in to vigilmon.online and click Add Monitor.
  2. Choose HTTP(S) monitor.
  3. Enter your health check URL: https://yourapp.example.com/health.
  4. Set the check interval (1 minute is the default).
  5. Add your email or Slack webhook as the alert destination.
  6. Click Save.

Vigilmon will start pinging /health every minute. If it receives a non-2xx response or the request times out, you get an alert immediately.

Webhook alerts

For more flexibility — posting to Slack, triggering a PagerDuty incident, or calling your own endpoint — add a webhook alert:

  1. In your monitor settings, click Add alert channel.
  2. Choose Webhook.
  3. Enter your webhook URL (e.g., a Slack incoming webhook, or your own handler endpoint).

Vigilmon will POST a JSON payload like this when a monitor transitions to DOWN or UP:

{
  "monitor_id": "mon_abc123",
  "monitor_name": "Production /health",
  "status": "down",
  "url": "https://yourapp.example.com/health",
  "checked_at": "2026-06-29T07:01:00Z",
  "response_code": 503,
  "response_time_ms": 1204
}

You can receive this in a Symfony controller and fan it out to any internal system:

<?php
// src/Controller/VigilmonWebhookController.php

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class VigilmonWebhookController
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    #[Route('/webhook/vigilmon', name: 'vigilmon_webhook', methods: ['POST'])]
    public function __invoke(Request $request): Response
    {
        $payload = json_decode($request->getContent(), true);

        if (($payload['status'] ?? '') === 'down') {
            $this->logger->critical('Monitor DOWN', [
                'monitor' => $payload['monitor_name'],
                'url'     => $payload['url'],
                'code'    => $payload['response_code'],
            ]);

            // Dispatch a Symfony event, send an email, page on-call — your logic here
        }

        return new Response('', Response::HTTP_NO_CONTENT);
    }
}

Part 3: Heartbeat monitoring for Symfony Messenger workers

HTTP checks only verify that the web tier responds. They will not catch a silent Messenger worker failure — when your queued jobs stop processing but your /health endpoint still returns 200.

A heartbeat monitor solves this. You configure a URL that Vigilmon expects to be called on a schedule. If the call stops arriving, Vigilmon fires an alert.

Create the heartbeat monitor

  1. In Vigilmon, click Add Monitor.
  2. Choose Heartbeat monitor.
  3. Set the interval to 5 minutes (choose a value longer than your worker's typical cycle time).
  4. Copy the unique heartbeat URL, e.g.: https://vigilmon.online/heartbeat/abc123xyz

Ping the heartbeat from your Messenger worker

Create a Symfony Messenger middleware that pings the heartbeat URL after each batch of messages is processed:

<?php
// src/Messenger/HeartbeatMiddleware.php

namespace App\Messenger;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Psr\Log\LoggerInterface;

class HeartbeatMiddleware implements MiddlewareInterface
{
    private int $messageCount = 0;
    private const PING_EVERY = 10; // ping after every 10 messages

    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly LoggerInterface $logger,
        private readonly string $heartbeatUrl,
    ) {}

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        $envelope = $stack->next()->handle($envelope, $stack);

        $this->messageCount++;

        if ($this->messageCount % self::PING_EVERY === 0) {
            $this->ping();
        }

        return $envelope;
    }

    private function ping(): void
    {
        try {
            $this->httpClient->request('GET', $this->heartbeatUrl, [
                'timeout' => 5,
            ]);
        } catch (\Throwable $e) {
            $this->logger->warning('Vigilmon heartbeat ping failed', [
                'error' => $e->getMessage(),
            ]);
        }
    }
}

Register the middleware and inject the heartbeat URL:

# config/services.yaml
services:
    App\Messenger\HeartbeatMiddleware:
        arguments:
            $heartbeatUrl: '%env(VIGILMON_HEARTBEAT_URL)%'
# config/packages/messenger.yaml
framework:
    messenger:
        buses:
            messenger.bus.default:
                middleware:
                    - App\Messenger\HeartbeatMiddleware

Add the environment variable:

# .env.local
VIGILMON_HEARTBEAT_URL=https://vigilmon.online/heartbeat/abc123xyz

Alternative: Scheduled command heartbeat

If you use Symfony Scheduler or a cron job rather than Messenger, ping the heartbeat at the end of your scheduled command:

<?php
// src/Command/ProcessReportsCommand.php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsCommand(name: 'app:process-reports')]
class ProcessReportsCommand extends Command
{
    public function __construct(
        private readonly HttpClientInterface $httpClient,
        private readonly string $heartbeatUrl,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // ... your report processing logic

        // Ping Vigilmon heartbeat to confirm successful run
        $this->httpClient->request('GET', $this->heartbeatUrl, ['timeout' => 5]);

        return Command::SUCCESS;
    }
}

Add this command to your crontab:

*/5 * * * * /path/to/app/bin/console app:process-reports >> /var/log/reports.log 2>&1

Vigilmon's heartbeat interval is also set to 5 minutes. If the cron stops running for any reason (server overload, failed deployment, accidental deletion), Vigilmon fires an alert within one interval window.


Part 4: Advanced health checks

Cache (Redis/Memcached) check

use Symfony\Component\Cache\Adapter\RedisAdapter;

try {
    $cache = RedisAdapter::createConnection($this->redisUrl);
    $cache->ping();
    $checks['cache'] = 'ok';
} catch (\Throwable $e) {
    $checks['cache'] = 'error';
    $status = 'degraded';
}

Queue depth check

If your Messenger queue backs up significantly, your workers may be overloaded. Return a warning in the health check:

$pendingCount = $this->entityManager
    ->getRepository(MessengerMessage::class)
    ->count([]);

$checks['queue_depth'] = $pendingCount;

if ($pendingCount > 1000) {
    $status = 'degraded';
}

Summary

With these three components in place your Symfony application now:

  1. Exposes /health — a real-time indicator of web tier and database status.
  2. Sends webhook alerts to Slack, PagerDuty, or your own endpoint when status changes.
  3. Pings a heartbeat URL from Messenger workers or scheduled commands, so silent worker death triggers an alert.

This is the minimum viable observability setup for a production Symfony application. Vigilmon handles the scheduling, alerting, and history — you just write the health logic that fits your application.


Monitor your Symfony app free at vigilmon.online

#php #symfony #monitoring #devops

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →