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
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter your health check URL:
https://yourapp.example.com/health. - Set the check interval (1 minute is the default).
- Add your email or Slack webhook as the alert destination.
- 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:
- In your monitor settings, click Add alert channel.
- Choose Webhook.
- 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
- In Vigilmon, click Add Monitor.
- Choose Heartbeat monitor.
- Set the interval to 5 minutes (choose a value longer than your worker's typical cycle time).
- 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:
- Exposes
/health— a real-time indicator of web tier and database status. - Sends webhook alerts to Slack, PagerDuty, or your own endpoint when status changes.
- 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