August 11, 2026 6 min read
How to monitor Heroku Scheduler jobs
Heroku Scheduler is documented as 'expected but not guaranteed'. Here's what that means in practice, what Heroku's native tooling misses, and how to know immediately when a scheduled job stops running.
Heroku Scheduler's documentation contains a sentence worth reading carefully:
Scheduler job execution is expected but not guaranteed. Scheduler is known to occasionally (but rarely) miss the execution of scheduled jobs.
That sentence has real consequences for anyone running critical jobs on Heroku Scheduler. It means that a nightly backup, a daily sync, or a billing job can silently not run — with no native alert, no notification, and no indication in any Heroku dashboard unless you go looking.
Heroku's own recommendation for critical jobs is to run a custom clock process instead of Scheduler. That's the right architectural advice. But many teams are running Scheduler for jobs that matter, and replacing it with a clock process is a non-trivial change. External monitoring is the practical bridge: it tells you when a job stops running regardless of the underlying cause.
What Heroku Scheduler doesn't provide
No missed run alerts. If Scheduler skips a job execution — due to platform load, a deployment issue, or the documented but-rarely case — no notification is sent. The job simply doesn't run, and the next scheduled execution proceeds as if nothing happened.
No failure alerts by default. When a job exits non-zero, Heroku logs it. If you have email notifications configured, you may receive an alert — but the default configuration and delivery are not reliable enough for production-critical jobs.
No hung job detection. If a job starts and never exits, Heroku has no concept of a maximum duration threshold. The dyno runs, billing accumulates, and no alert fires.
No output validation. Heroku Scheduler considers a zero exit code a successful run. A job that processes no records, produces an empty export, or sends no emails is indistinguishable from a successful run.
Adding external monitoring to Heroku Scheduler jobs
The pattern works for any language Heroku supports. Your job sends HTTP pings to an external monitor at start and completion. The external monitor holds the expected schedule and fires an alert if pings don't arrive on time.
For a Node.js job:
import { CrontifyMonitor } from '@crontify/sdk';
const monitor = new CrontifyMonitor({
apiKey: process.env.CRONTIFY_API_KEY!,
monitorId: process.env.CRONTIFY_MONITOR_ID!,
});
async function main(): Promise<void> {
await monitor.wrap(async () => {
const result = await runScheduledJob();
return {
meta: {
records_processed: result.count,
errors: result.errors,
}
};
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
For a Ruby job:
require 'net/http'
require 'json'
require 'uri'
def ping(event, payload = {})
uri = URI("https://api.crontify.com/api/v1/ping/#{ENV['CRONTIFY_MONITOR_ID']}/#{event}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 5
http.read_timeout = 5
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = ENV['CRONTIFY_API_KEY']
request['Content-Type'] = 'application/json'
request.body = payload.to_json unless payload.empty?
http.request(request)
rescue StandardError
# Never let monitoring failure kill the job
end
ping('start')
begin
result = run_job
ping('success', { meta: { records_processed: result[:count] } })
rescue => e
ping('fail', { message: e.message, log: e.backtrace.first(15).join("\n") })
exit 1
end
For a Python job:
import os
import sys
import requests
import traceback
def ping(event, payload=None):
try:
requests.post(
f"https://api.crontify.com/api/v1/ping/{os.environ['CRONTIFY_MONITOR_ID']}/{event}",
headers={"X-API-Key": os.environ["CRONTIFY_API_KEY"]},
json=payload,
timeout=10,
)
except Exception:
pass
ping("start")
try:
result = run_job()
ping("success", {"meta": {"records_processed": result.count}})
except Exception as exc:
ping("fail", {"message": str(exc), "log": traceback.format_exc()})
sys.exit(1)
Set CRONTIFY_API_KEY and CRONTIFY_MONITOR_ID as Heroku config vars:
heroku config:set CRONTIFY_API_KEY=ck_live_your_key
heroku config:set CRONTIFY_MONITOR_ID=your-monitor-id
Handling Heroku Scheduler's limited schedule options
Heroku Scheduler only supports three frequencies: every 10 minutes, every hour, or every day at a specified time. Configure your Crontify monitor with the equivalent cron expression:
| Heroku Scheduler setting | Crontify cron expression |
|---|---|
| Every 10 minutes | */10 * * * * |
| Every hour (at :00) | 0 * * * * |
| Every day at 02:00 UTC | 0 2 * * * |
Set a grace period of at least 5 minutes for Heroku Scheduler — execution times are not guaranteed to the minute.
What external monitoring catches that Heroku's native tooling misses
The documented missed run. Heroku acknowledges Scheduler can skip executions. External monitoring detects the absent ping within minutes of the expected time.
The silent output failure. Heroku considers exit code 0 a success. Crontify's alert rules on output metadata fire when records_processed eq 0 — even though Heroku saw a clean exit.
The hung job. A start ping arrives. No success ping follows within the configured threshold. Crontify fires a hung job alert. Heroku has no equivalent mechanism.
The platform incident. In June 2025, Heroku experienced a multi-hour outage. External monitoring — running on independent infrastructure — was the only signal available during that window.
Crontify is free for up to 5 monitors — no credit card required.
Start monitoring your scheduled jobs
Free plan includes 5 monitors. No credit card required. Up and running in under 5 minutes.
Get started free →More from the blog
August 11, 2026 7 min read
Cron job monitoring for data pipelines and ETL jobs
Data pipeline failures are uniquely damaging because they accumulate silently. Standard cron monitoring catches crashes — here's how to catch the failures that don't crash.
Read more →
August 11, 2026 7 min read
How to get alerts when pg_cron jobs fail
pg_cron logs failures to cron.job_run_details but sends no external alert. Here's how to add missed run detection, failure alerts, and silent failure detection to PostgreSQL scheduled jobs.
Read more →