Documentation

Snippets

Pick your runtime, paste the check UUID, and monitoring is done. Every snippet sends three signals, not just success.

Why three signals and not one

  • /start before the job — this is what gives you the runtime: the chart is built from start → success pairs;
  • success (the bare URL) or the exit code — closes the pair and confirms the run went through;
  • /fail or a non-zero code — drops the check to down immediately, skipping grace: the job said it failed, there is nothing to wait for.

The full suffix table and the rules live on /start, /fail and duration.

Code

#!/usr/bin/env bash
PING=https://ping.cronalive.com/<uuid>

# A ping must never break the job: || true swallows a network error.
signal() { curl -fsS -m 10 --retry 3 -o /dev/null "$PING$1" || true; }

signal /start
/usr/local/bin/backup.sh
signal "/$?"   # 0 is success, any other code is a failure

Put the whole script in cron: 30 2 * * * /usr/local/bin/backup-monitored.sh

import subprocess
import urllib.request

PING = "https://ping.cronalive.com/<uuid>"

def signal(path: str = "") -> None:
    try:
        urllib.request.urlopen(PING + path, timeout=5)
    except Exception:
        pass  # monitoring must not break the job

signal("/start")
code = subprocess.call(["/usr/local/bin/backup.sh"])
signal(f"/{code}")

Packaged: pip install cronalive, then the @cronalive.monitor("<uuid>") decorator or the cronalive run --id <uuid> -- cmd wrapper.

const PING = 'https://ping.cronalive.com/<uuid>';

// AbortSignal.timeout is a total budget: DNS, connect and response.
const signal = (path = '') => fetch(PING + path, {
    signal: AbortSignal.timeout(5_000),
}).catch(() => {});

await signal('/start');
try {
    await runJob();
    await signal();
} catch (error) {
    await signal('/fail');
    throw error;
}

Packaged: npm i cronalive, then await monitored("<uuid>", () => runJob()).

<?php

$ping = 'https://ping.cronalive.com/<uuid>';

$signal = static function (string $path = '') use ($ping): void {
    $context = stream_context_create(['http' => [
        'timeout' => 5,
        'ignore_errors' => true,
    ]]);

    @file_get_contents($ping.$path, false, $context);
};

$signal('/start');

try {
    run_job();
    $signal();
} catch (Throwable $e) {
    $signal('/fail');
    throw $e;
}
composer require cronalive/laravel

# .env
CRONALIVE_PING_DOMAIN=https://ping.cronalive.com

// routes/console.php — signals are capped at 2s connect / 5s total
$schedule->command('my:job')
    ->hourly()
    ->pingCronalive('<uuid>');

Laravel 10 through 13, PHP 8.1+. The macro sends /start before the job and success or /fail after it — nothing to add by hand. Laravel's own thenPing does not, and waits up to 30 seconds per signal.

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStartPre=/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null https://ping.cronalive.com/<uuid>/start
ExecStart=/usr/local/bin/backup.sh
ExecStopPost=/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null https://ping.cronalive.com/<uuid>/$EXIT_STATUS

ExecStopPost runs whatever the outcome, and $EXIT_STATUS holds the exit code — no separate failure branch needed.

name: nightly-backup

on:
  schedule:
    - cron: '30 2 * * *'

env:
  PING: https://ping.cronalive.com/<uuid>

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - run: curl -fsS -m 10 --retry 3 -o /dev/null "$PING/start"

      - run: ./scripts/backup.sh

      # always() — this step runs even after the previous one failed.
      - if: always()
        env:
          CODE: ${{ job.status == 'success' && '0' || '1' }}
        run: curl -fsS -m 10 --retry 3 -o /dev/null "$PING/$CODE"

The GitHub Actions scheduler is not reliable on its own: cron runs are regularly delayed and occasionally skipped. That is exactly why the check deserves a generous grace period.

The same snippets in the dashboard

On the check page the UUID is already filled in, and the ready-made status badge URL sits right next to it — nothing to copy by hand:

Rules that apply everywhere

  • a ping must not break the job — swallow the error (|| true, try/except, .catch());
  • give every request an explicit short timeout; without one a hung socket holds the job;
  • do not retry forever: a lost signal is recovered by the next run;
  • you can address a check by slug instead of UUID if spreading UUIDs across servers is awkward.

Details: Ping reliability.