Pick your runtime, paste the check UUID, and monitoring is done. Every snippet sends three signals, not just success.
The full suffix table and the rules live on /start, /fail and duration.
#!/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>'); 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.
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:
|| true, try/except, .catch());Details: Ping reliability.