Heartbeat monitoring for scheduled jobs on a PaaS

Platform cron features tell you about runs that started and failed. They rarely tell you about runs that never started. Add a heartbeat: the job pings a URL when it starts and when it ends, with its exit code. If a ping does not arrive by the scheduled time plus a grace period, or the job runs too long, you get an alert.

· Updated · commands checked against current releases

What can go wrong with scheduled jobs on a PaaS

  • The run never starts. A failed build, a removed schedule, a suspended service or a stopped container means no run and no log line. Failure notifications only fire for runs that happened.
  • A run hangs and blocks the next ones. A job that never exits holds its slot. Railway skips the next run while one is still active, and Render delays it until the active run finishes, so one stuck job quietly stops all later ones.
  • The job fails but looks fine. A script that catches every error and exits 0, or a shell pipeline that hides a failed command, reports success while doing nothing.
  • Wrong time zone. Render and Railway schedules are in UTC, while Coolify uses the server's time zone. A daily job set for 09:00 local time can run hours early or late.
  • Killed for memory. The container hits its memory limit and is killed during the run. Depending on the platform this shows as a crash, a failed run or nothing at all.
  • No network for the report. The job finished but could not reach the outside world, so its own failure email or webhook never left.

The signals that matter, with alert thresholds

SignalAlert whenWhy it matters
Finish pingnone by the scheduled time plus graceThe job did not run, did not finish, or could not reach the network.
Exit code pingany value from 1 to 255The job ran and failed; the output you send with it says why.
Run time from /start to finishlonger than the max run time you setA hung run blocks later runs on Render and Railway.
Deploy and build statusfailed build or crashed containerThe next run will use old code or not start at all.
Work done per run (by hand)zero rows, files or emails when there should be someMake the job exit non-zero when it did nothing, so this becomes an alert.

Thresholds are starting points. Tune them after a week of data: an alert nobody acts on should be raised or removed.

How to check scheduled jobs on a PaaS by hand

Does the ping URL work from your machine?
curl -fsS -m 10 https://puzaricloud.in/ping/<token>/start && sleep 5 && curl -fsS -m 10 https://puzaricloud.in/ping/<token>/0
Does the container have curl, wget or Python?
command -v curl wget python3 python node
Railway: list recent cron runs
railway cron list
Dokploy or Coolify host: is the target container running?
docker ps --format '{{.Names}}  {{.Status}}' | grep -i <app-name>

Replace <token> with the token from your heartbeat in Vigil. A ping returns OK; an unknown token returns 404.

How each platform runs scheduled jobs

PlatformWhere the schedule livesTime zoneWhen a run is still goingPut the wrapper in
RenderA Cron Job service with a Schedule and a CommandUTCAt most one run is active; the next scheduled run waits until it finishes. Render stops a run after 12 hours.The Command field, or the Docker Command under Advanced for image-based jobs
RailwayThe service's Cron Schedule setting; Railway runs the start commandUTC; runs at least 5 minutes apartIf the previous run is still Active, the new run is skipped. A job that never exits blocks every later run.The service's start command
CoolifyScheduled Tasks on the application or service; the command runs inside its containerThe time zone set on the serverCheck your version's behaviour; the heartbeat's max run time catches long runs either way.The task command, wrapped in sh -c '...'
DokploySchedule Jobs: application and compose jobs run with docker exec, server jobs run on the hostThe server's clockThe target container must be running, or the job fails to start.The job command, or the server script

From each platform's documentation on 20 October 2026. Platforms change; when a row matters to you, check the current docs.

One wrapper script for every platform

Put a tiny script in your repository and run your real command through it. It pings /start, runs the job, then pings /<exit code>: 0 marks the run as a success, 1 to 255 as a failure. The job's output goes to the platform's log as before and is also stored with the run.

heartbeat.sh
#!/bin/sh
# heartbeat.sh: ping /start, run the job, then report its exit code.
# usage: sh heartbeat.sh python manage.py send_digests
URL="https://puzaricloud.in/ping/$HEARTBEAT_TOKEN"
curl -fsS -m 10 --retry 3 -o /dev/null "$URL/start"
"$@" > /tmp/job.log 2>&1
code=$?
cat /tmp/job.log
# the last 100 KB of output is stored with the run in Vigil
tail -c 100000 /tmp/job.log | curl -fsS -m 10 --retry 3 -o /dev/null --data-binary @- "$URL/$code"
exit $code

Set HEARTBEAT_TOKEN as an environment variable on the service. Then the platform command becomes, for example, sh heartbeat.sh python manage.py send_digests on Render or Railway, or sh -c 'sh /app/heartbeat.sh php artisan reports:daily' in a Coolify scheduled task or a Dokploy schedule job.

The --retry 3 matters: one network blip at the end of a long job should not turn into a false "missed run" alert. The -m 10 time limit makes sure a slow network never keeps the job alive.

No curl in the image? Other ways to send the same ping
# Alpine and BusyBox images have wget
wget -q -T 10 -O /dev/null "https://puzaricloud.in/ping/$HEARTBEAT_TOKEN"

# slim Python images have neither: use Python itself
python -c "import sys, urllib.request; urllib.request.urlopen(sys.argv[1], timeout=10)" "https://puzaricloud.in/ping/$HEARTBEAT_TOKEN/$code"

# Node 18 and later
node -e "fetch(process.argv[1]).catch(() => process.exit(1))" "https://puzaricloud.in/ping/$HEARTBEAT_TOKEN"

The ping URLs follow the Healthchecks.io format, so existing Healthchecks client libraries and scripts work by changing the base URL.

How to watch scheduled jobs on a PaaS with Vigil

  1. In New monitor create a Heartbeat and choose Cron expression. Paste the job's schedule and set the time zone to match the platform: UTC for Render and Railway, your server's time zone for Coolify.
  2. Set a grace period longer than a normal run, for example 10 minutes for a job that takes 2, so a slow run is not reported as missed.
  3. Set Max run time. It needs the /start ping, which the wrapper sends, and alerts when a run is still going after that many minutes.
  4. Copy the ping URL, https://puzaricloud.in/ping/<token>, put its token in HEARTBEAT_TOKEN on the service, and run the job through the wrapper script above.
  5. Trigger one run by hand (Render: Trigger Run, Railway: railway cron run, Coolify: Execute Now) and check that the heartbeat shows the start and finish pings.
  6. Pick alert channels that do not depend on the platform, such as Telegram, Slack, email or on-call paging.

Every alert goes to the channels you pick: email, Slack, Telegram, Discord, Microsoft Teams, Pushover, webhooks or on-call paging. Failures are confirmed by a second check before anyone is woken up.

scheduled jobs on a PaaS monitoring FAQ

Do I still need the platform's own failure emails?

Keep them, they cost nothing. But they only cover runs that started. The heartbeat also covers runs that never started, runs that hung and runs whose failure report could not be sent.

What grace period should I use?

Longer than your slowest normal run plus a few minutes. A job that usually takes 3 minutes and sometimes 8 is fine with a grace of 15. If the alert comes too late for you, lower the grace and use max run time to catch hung runs.

My Railway cron runs every 5 minutes. Is a heartbeat too noisy?

No. A heartbeat stays silent while pings arrive on time and alerts once when they stop, then once when they resume. Railway's minimum gap is 5 minutes, so set the heartbeat's cron to the same schedule.

Can I use this without changing my code?

Yes. The wrapper runs your existing command unchanged; only the platform's command field changes. Heartbeats with a 1-minute period are on the free plan.

Vigil by PuzariCloud

Watch scheduled jobs on a PaaS without stitching five tools together

Uptime and API checks, heartbeats, server and database metrics, traces, errors and logs in one place, with one incident and one alert when something breaks. Every feature is on every plan, including Free.