Scheduled Job Heartbeat and Silence Ledger avatar

Scheduled Job Heartbeat and Silence Ledger

Pricing

from $20.00 / 1,000 run starteds

Go to Apify Store
Scheduled Job Heartbeat and Silence Ledger

Scheduled Job Heartbeat and Silence Ledger

Your cron job sends one curl ping after each run. This Actor keeps the ledger in a named key-value store and reports every job that went silent, with an optional webhook alert. HTTP only, no browser, no proxy, no database.

Pricing

from $20.00 / 1,000 run starteds

Rating

0.0

(0)

Developer

kingii98

kingii98

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

An uptime monitor tells you that a URL answers. It cannot tell you that your nightly backup stopped to run, because silence is the absence of an event and there is no URL to check.

This Actor gives you a heartbeat ledger with one curl call and no new vendor:

  1. Your cron job, backup script, ETL step or queue worker sends one ping after each successful run.
  2. The Actor writes the ping into a named Apify key-value store.
  3. A scheduled evaluate run compares the ledger against the gap that you declared for each job, and reports every job that went silent.

No Redis. No database. No monitoring SaaS. No browser and no proxy.

How it works

The Actor has two modes.

ModeHow you start itWhat it does
receiveApify StandbyHolds an HTTP server open and accepts POST /ping/<slug>.
evaluateApify Scheduler, every 15 minutesReads the ledger, judges every job, writes the dataset and sends the alerts.

Both modes must use the same ledgerStoreName, because that named store is the ledger. A run in Standby always uses the receive mode, whatever the mode field holds.

1. Turn on Standby and send the pings

Open Settings → Standby of your Actor build and switch Standby on. Apify then gives you a Standby URL. Add one line to the end of your job:

# crontab: run the backup, and ping only when it succeeded
0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 \
"https://<your-standby-url>/ping/nightly-backup?secret=$PING_SECRET"

GET and POST are both accepted, so a plain curl -fsS <url> also works. The secret may travel as ?secret=…, as the header X-Ping-Secret, or as Authorization: Bearer ….

AnswerMeaning
200The ping is in the ledger.
401The secret is wrong or missing.
404The slug is not in the jobs list of the Standby run.
405The method is not GET or POST.
413The request passes maxRequestBytes.

GET / answers a small JSON health record with the list of known slugs. It needs no secret, so the Apify Standby probe can reach it.

2. Schedule the evaluate run

Add an Apify schedule that starts this Actor every 15 minutes with "mode": "evaluate" and the same jobs and ledgerStoreName.

Put every job into one schedule, because run-started is charged for each evaluate run and not for each job. The cadence sets both the bill and the alert latency. Read Pricing before you choose it.

Status rules

For each job, with gap = expected_max_gap_minutes and grace = grace_minutes:

Silence since the last pingStatus
<= gapok
> gap and <= gap + gracelate
> gap + gracemissed

A job that the ledger sees for the first time stays ok until its own first deadline runs out, so a job that you added one minute ago does not alert at once. consecutiveMisses counts up on every missed evaluation, holds its value on late, and returns to 0 on the first new ping.

Input

FieldTypeDefaultDescription
jobsarraythree sample jobs1 to 50 records. Each holds slug, name, expected_max_gap_minutes and the optional grace_minutes.
modeselectevaluateevaluate or receive. A Standby run always receives.
ledgerStoreNamestringheartbeat-ledgerThe named key-value store that holds the ledger.
pingSecretstring (secret)noneOptional shared secret for the ping caller.
alertWebhookUrlstringemptyOptional public HTTPS URL. One JSON POST for each job that becomes late or missed.
defaultGraceMinutesinteger5The grace for a job record without grace_minutes.
suppressRepeatAlertsbooleantrueOne alert for each status change, and not one on every tick.
timeoutSecondsinteger15Timeout for one alert POST.
receiveMaxSecondsinteger60How long a manual receive run stays open. Standby ignores it.
maxRequestBytesinteger16384Byte cap for one ping request.

A slug holds 1 to 64 characters out of a-z, 0-9, ., - and _, and it starts with a letter or a digit. It travels in the ping URL and in the store key, so no other character is accepted.

Example input

{
"jobs": [
{
"slug": "nightly-backup",
"name": "Nightly database backup",
"expected_max_gap_minutes": 1440,
"grace_minutes": 30
},
{
"slug": "queue-worker",
"name": "Queue worker heartbeat",
"expected_max_gap_minutes": 15,
"grace_minutes": 5
}
],
"mode": "evaluate",
"ledgerStoreName": "heartbeat-ledger",
"alertWebhookUrl": "https://hooks.example.com/cron-silence"
}

Set expected_max_gap_minutes a little above the real cadence of the job. A nightly job that starts at 02:00 is well served by 1440 with a grace of 30, because a slow run must not raise an alert.

Output

One job record for each job in each evaluate run:

FieldDescription
slug, nameIdentity of the job.
statusok, late or missed.
lastPingAt, minutesSinceLastPing, pingCountWhat the ledger holds.
expectedMaxGapMinutes, graceMinutesThe rule that the status came from.
consecutiveMissesHow many evaluations in a row saw this job as missed.
alert, alertSuppressedWhether this run raised an alert for this job.
firstSeenAt, note, checkedAtWhen the ledger first saw the job, and why.

One alert record for each alert:

FieldDescription
slug, name, status, minutesSinceLastPing, consecutiveMissesThe job that went silent.
webhookStatusThe HTTP status code of the POST, or null when no webhook URL is set.
delivered, note, dispatchedAtThe outcome of the POST.

One summary record for each run:

{
"recordType": "summary",
"mode": "evaluate",
"jobCount": 3,
"okCount": 2,
"lateCount": 0,
"missedCount": 1,
"allOk": false,
"alertsRaised": 1,
"alertsDispatched": 1,
"jobDaysCharged": 3,
"note": "",
"finishedAt": "2026-09-03T12:00:00Z"
}

Alert payload

{
"source": "scheduled-job-heartbeat-and-silence-ledger",
"event": "job-silence-alert",
"slug": "nightly-backup",
"name": "Nightly database backup",
"status": "missed",
"lastPingAt": "2026-09-02T02:03:11Z",
"minutesSinceLastPing": 1980.4,
"expectedMaxGapMinutes": 1440,
"graceMinutes": 30,
"consecutiveMisses": 2,
"checkedAt": "2026-09-03T11:03:33Z"
}

Pricing

This Actor uses the pay-per-event model. Four events are charged:

EventUnitPrice (USD)When the Actor charges it
run-startedone evaluate run0.02Once for each evaluate run, after the input is read. A receive run never charges it.
heartbeat-receivedone accepted ping0.0008Once for each ping that the receive mode wrote into the ledger. A refused ping (401, 404, 413) is not charged.
job-day-monitoredone job for one calendar day0.08Once for each job on its first evaluate run of a UTC day. The ledger holds the last charged day, so 96 evaluations in a day charge one job once.
alert-dispatchedone webhook POST0.05Once for each alert POST that your webhook answered. An alert without a webhook URL, and a POST that could not reach the host, are not charged.

What one month costs

run-started is charged for each evaluate run, and not for each job. A 15 minute schedule makes 96 evaluate runs for each day, so this event sets the floor of the bill. Every job that you add to the same run shares that floor.

One job that pings every hour, evaluated every 15 minutes, across 30 days:

EventCountPrice (USD)Cost (USD)
run-started96 runs x 30 days = 28800.0257.60
job-day-monitored1 job x 30 days = 300.082.40
heartbeat-received24 pings x 30 days = 7200.00080.58
alert-dispatched00.050.00
Total60.58

The schedule cadence sets both the bill and the alert latency, because the Actor finds a silent job on its next evaluate run. A slower schedule costs much less, and more jobs in one run make each job cheaper:

Evaluate scheduleRuns for each day1 job10 jobsFor each job, at 10 jobs
every 15 minutes9660.5887.368.74
every hour2417.3844.164.42
every 6 hours45.3832.163.22

All numbers are USD for each month of 30 days, with one hourly ping for each job and no alert. Put every job into one schedule, and choose the slowest cadence that still finds a silent job soon enough for you.

A business verdict is not a failure

A late job, a missed job, a refused webhook URL and a run that finds nothing are all dataset records plus a status message. The run ends SUCCEEDED. Only a real malfunction, such as an input that cannot be read, ends FAILED.

Limits

  • HTTP only. No browser, no proxy, no CAPTCHA service, no paid API and no external database.
  • The alert webhook must be a public HTTPS URL. Loopback, private and reserved addresses are refused, and the POST never follows a redirect.
  • 1 to 50 jobs for each run.
  • The receive mode answers one request for each connection and then closes it.

Development

uv sync
uv run pytest
uv run ruff check .