Monitor the jobs nobody watches
Uptime checks tell you when a website stops answering. They say nothing about the nightly backup that has been failing since the last server upgrade, the queue worker that died three weeks ago, or the AI agent that stopped its run halfway through. Those jobs fail quietly — there is no error page to see.
Avospy turns that around: every job gets its own ping URL. The job calls it when it finishes, and when a ping does not arrive in time, you hear about it.
1. Create a monitor
Sign in and open Heartbeats → New Monitor. Name it after the job it watches (“shop.cz — nightly backup”), then set two numbers:
- Expected interval — how often the job should ping, in minutes.
- Grace period — how much lateness is still fine, in minutes. The default is 5.
You get back a ping URL that looks like this:
https://avospy.com/api/ping/YOUR_TOKENThe clock starts immediately, before the first ping ever arrives — a fresh monitor with a 60-minute interval and 5 minutes of grace will report a miss 65 minutes after you created it. Wire the job up right away, or expect one alert to teach you that the monitor works.
2. Ping it when the job finishes
The ping is a plain GET request — no headers, no body, no signature. Anything that can make an HTTP call can do it, and the endpoint answers {"ok":true}. Pick whichever of these matches your stack:
# nightly backup — pings only when the backup itself succeeded
0 3 * * * /usr/local/bin/backup.sh && /usr/bin/curl -fsS -m 10 --retry 3 https://avospy.com/api/ping/YOUR_TOKEN >/dev/null 2>&1#!/usr/bin/env bash
set -euo pipefail
PING="https://avospy.com/api/ping/YOUR_TOKEN"
start=$(date +%s)
pg_dump mydb | gzip > /backups/mydb.sql.gz # the actual work
curl -fsS -m 10 --retry 3 \
"$PING?duration=$(( ($(date +%s) - start) * 1000 ))&msg=backup+ok" >/dev/null || true# /etc/systemd/system/import.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/import.sh
# runs only if ExecStart finished successfully
ExecStartPost=/usr/bin/curl -fsS -m 10 --retry 3 https://avospy.com/api/ping/YOUR_TOKEN- name: Nightly sync
run: ./scripts/sync.sh
- name: Heartbeat
if: success()
run: curl -fsS -m 10 --retry 3 "${{ secrets.PING_URL }}"const started = Date.now();
await runJob();
// fire-and-forget: a failed ping must never fail the job
await fetch(`${process.env.PING_URL}?duration=${Date.now() - started}`, {
signal: AbortSignal.timeout(10_000),
}).catch(() => {});import time, urllib.request
started = time.time()
run_job()
try:
urllib.request.urlopen(
f"{PING_URL}?duration={int((time.time() - started) * 1000)}", timeout=10
)
except Exception:
pass # the ping must never break the jobSchedule::command('sitemap:generate')
->dailyAt('04:00')
->thenPing('https://avospy.com/api/ping/YOUR_TOKEN')
->pingOnFailure('https://avospy.com/api/ping/YOUR_TOKEN?status=fail');3. Send a little context with it
Three optional query parameters turn the ping history from “something ran” into a log you can actually read:
| Parameter | Value | What it is for |
|---|---|---|
msg | any text | A short note stored with the ping and shown in the history — “1842 rows imported”, the release tag, the host name. URL-encode it. |
duration | milliseconds | How long the run took. Shown next to the ping, so a job that slowly creeps from 20 s to 4 minutes is visible before it starts timing out. |
status | ok | fail | Defaults to ok. A fail ping is recorded as a failed run and turns the monitor red immediately — useful when the job knows it broke. |
curl -fsS "https://avospy.com/api/ping/YOUR_TOKEN?status=ok&duration=48210&msg=1842+rows+imported"The last twenty pings of each monitor are listed under “Show history”, newest first, with their duration and message.
4. Choosing the interval and the grace period
A monitor is late once interval + grace period have passed since its last ping. The grace period is there for the normal jitter of real systems: a queue that was busy, a backup that had more data than usual, a runner that waited for a free slot.
| Job | Interval | Grace | Alert after |
|---|---|---|---|
| Nightly database backup at 03:00 | 1440 min | 120 min | 1560 min |
| Hourly feed or XML import | 60 min | 15 min | 75 min |
| Queue worker that pings every 5 minutes | 5 min | 5 min | 10 min |
| Weekly invoicing run on Monday | 10080 min | 180 min | 10260 min |
Deadlines are evaluated every five minutes, so an alert can be up to five minutes later than the arithmetic suggests — a grace period under five minutes buys you no extra precision. When in doubt, start generous: a monitor that cries wolf gets muted, and a muted monitor protects nothing.
5. What happens when a ping goes missing
The monitor turns red
Its state changes to down on the Heartbeats screen, with the time of the last ping it did receive.
You get one alert, not a stream
The alert fires on the transition from healthy to missed — an e-mail plus an entry under Notifications. It is not repeated every five minutes while the job stays down, and a repeat alert within an hour of the previous one is held back, so a flapping job cannot flood your inbox.
The next ping clears it
As soon as a ping arrives, the monitor is green again and the clock restarts. Recovery is silent — the Heartbeats screen is where you confirm it.
6. Six rules that keep heartbeats honest
- Ping at the end of the job, not at the start. A ping at the start only proves the scheduler fired — the interesting failure is the job that starts and never finishes.
- Let the job decide. Chain the ping with && (or run it only on success in CI) so a crashed job stays silent and the missed deadline raises the alarm.
- Never let the ping break the job. Use a timeout and a retry, and swallow its exit code: curl -fsS -m 10 --retry 3 … || true.
- One monitor per job. If two cron entries share a URL, one of them running is enough to keep the monitor green — and the broken one stays invisible.
- Treat the ping URL as a secret. Anyone who has it can keep your monitor green, so keep it in CI secrets or an env var, not in a public repository.
- Planned downtime: delete the monitor, or widen the interval before the maintenance window. There is no snooze button yet.
7. When something does not work
The ping returns 404 “Monitor not found”
The token is truncated or the monitor was deleted or switched off. Copy the URL again from the Heartbeats screen and call it once by hand.
Cron runs the job, but no ping arrives
Cron runs with a minimal PATH and no shell profile. Use the full path to curl (/usr/bin/curl), put the whole command on one line, and test that exact line in a plain shell first.
An alert arrived even though the job ran
The deadline counts from the last ping, not from the scheduled time. A run that started late or took longer than the interval plus grace period misses the window — widen the grace period, or ping from inside long runs as well.
The monitor went red and no e-mail came
The alert is always recorded under Notifications even when the e-mail cannot be delivered. Check the address on your account there first.
Still stuck? Write to support@avospy.com and include the name of the monitor.
Put your first cron job on watch
Create an account, add a monitor, paste one curl line into your crontab — the next silent failure will not stay silent.