Skip to content

Heartbeats

The SDK can ping a heartbeat on the Quiet Guard server every time one of your scheduled tasks runs. The server works as a dead-man's switch: once the heartbeat is armed with an expected period, missing pings trigger an overdue alert. A crashed cron entry, a forgotten schedule:run, a task disabled "temporarily": silence itself becomes the alert. The full concept (statuses, arming, alert channels) is covered in the server documentation.

The scheduler macro (preferred)

The SDK registers a monitorHeartbeat() macro on Laravel's scheduled events. Chain it directly on the task you want to watch, in routes/console.php:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:send')->daily()->monitorHeartbeat('reports-send');

Two properties make the macro the recommended wiring:

  • It pings only when the task succeeds (it hooks the scheduler's onSuccess). A task that crashes or exits non-zero sends nothing, stays silent, and therefore triggers the overdue alert. Pinging before the task, or unconditionally after it, would defeat the whole point.
  • It honours the SDK's gates at run time: when MONITOR_ENABLED=false, or when the current environment is not in the MONITOR_ENVIRONMENTS allowlist, the ping is skipped. A staging scheduler running the same code therefore never pings your production heartbeats. See Configuration.

Manual ping

You can also ping from any code path with the Artisan command, for example from a raw cron line after a shell script:

bash
php artisan monitor:heartbeat reports-send

When MONITOR_ENABLED=false, the command prints a warning and sends nothing. It exits with a failure code when the ping cannot be delivered (check MONITOR_URL / MONITOR_KEY).

Automatic heartbeats

Since version 0.4, the SDK watches two things on its own, in production, without a line from you:

  • The scheduler. Every time schedule:run starts (from cron, and in each subprocess schedule:work spawns), it pings the scheduler heartbeat. The first start only records the time; from the second on, the ping carries the observed gap as the period (1 to 1440 minutes) and a grace of period plus 4 minutes, capped at 1440 minutes. So with a cron every minute the heartbeat appears after two minutes; with an hourly cron, after two hours. A schedule:run typed by hand in a terminal is ignored, neither recorded nor pinged, so a manual test can never arm a wrong cadence. Several servers running the same cron on one shared cache count as one scheduler: a start less than 30 seconds from the recorded one is taken for that same start.
  • Your queues. Every queue:work worker (Horizon included) pings, on every turn of its loop and even with no job to process, a queue-{connection}-{queue} heartbeat for each queue it consumes and that is watched, at most once a minute per queue. Nothing is added to the queue, no probe job: the ping comes from the loop turn itself. The grace is nine minutes, or the worker's --timeout plus one minute when that is longer (Laravel 13.10 or later; earlier versions keep nine minutes), capped at 1440 minutes. An overdue queue heartbeat alerts on its own, independently of the scheduler. The queue lag shown on the Heartbeats tab is the shortest wait observed, since the previous ping, between a job being queued and a worker taking it; a missing value means the worker that sent the ping measured none, while other workers of the queue may have taken jobs, and a retry is never measured. On Laravel 11, and on 12 before 12.11, whose job payload carries no creation timestamp, nothing is measured and the lag stays empty.

These heartbeats arm themselves on the first ping that carries enough to arm with: nothing is created until the scheduler or a worker has run at least once, and an application without queues never gets an alert about them. The period and grace are fixed at creation: the SDK never re-arms an existing heartbeat, including its own.

To watch other queues, or none:

dotenv
# Several queues, as connection:queue
MONITOR_QUEUE_HEARTBEATS=redis:default,redis:emails
# No queue
MONITOR_QUEUE_HEARTBEATS=false
# No scheduler heartbeat
MONITOR_SCHEDULER_HEARTBEAT=false
# Environments concerned (production by default)
MONITOR_HEARTBEAT_ENVIRONMENTS=production

These heartbeats use your default cache store. The scheduler's needs one that survives the process (file, database, Redis): with the array store each schedule:run starts from an empty cache, so the scheduler heartbeat is never sent, a safe and silent failure. Queue pings still go out on the array store, but each worker process then pings on its own instead of one ping per queue for all the workers. Changing MONITOR_KEY changes the cache keys, so the next start counts as a first one again. A worker whose ping fails sends no queue ping for the next minute: a Quiet Guard server that cannot be reached costs each worker one MONITOR_TIMEOUT a minute, whatever the number of queues. To switch an automatic heartbeat off, change the variable, then delete the row on the Heartbeats tab: the SDK never re-arms an existing row.

A queue heartbeat can go overdue with nothing broken, because a worker only pings between two jobs, and only while it loops. The known cases:

  • maintenance mode lasting longer than the grace: the worker stops looping until the application is back up, unless it runs with --force;
  • a paused worker, through php artisan horizon:pause or a SIGUSR2 signal sent to queue:work;
  • a single job running longer than the grace, including a job whose own $timeout exceeds the worker's --timeout: the grace follows the worker's option, while the job's own value wins at run time;
  • workers started only from time to time, for example queue:work --stop-when-empty from cron or from the scheduler, less often than the grace (nine minutes by default);
  • queue:listen and queue:work --once, which never loop and so never ping: a queue that moves to them after a queue:work created its heartbeat goes overdue.

In each of these cases, widen that heartbeat's grace on the Heartbeats tab (the SDK never re-arms an existing row, so your value stays), or leave the queue out of MONITOR_QUEUE_HEARTBEATS and delete its row.

Lifecycle

  • The first ping auto-registers the heartbeat on your project. One wired with the macro or the command is born unarmed: pings are recorded and nothing ever alerts until you set a period. Automatic heartbeats are the exception: they arrive with their period and are born armed.
  • Arming happens in the dashboard: open the project's Heartbeats tab and set the expected period (plus a grace margin). The SDK only arms its own automatic heartbeats, and only when they are created.
  • Recovery is silent: after an outage, the next successful ping returns the heartbeat to healthy without a notification.

Slug rules

A heartbeat slug identifies the task: lowercase letters, digits and single dashes (reports-send, nightly-backup). The server normalises anything else, so use that charset from the start and keep the slug identical between the macro and the dashboard.

Next step

Continue with Encrypted backups, the natural companion: a heartbeat on your backup task tells you the day it stops running.

You are reading the Laravel SDK v1.0 documentation.