Get started
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

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>');

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

Details: Ping reliability.