Skip to content
← Guides

Your Heroku Scheduler job didn't run, and nothing told you.

Heroku Scheduler is the quick way to run a script or AI agent every 10 minutes, hourly or daily on Heroku. But Heroku's own docs say Scheduler doesn't guarantee a job runs, and a skipped run leaves nothing in your inbox. Here are the usual causes, straight from Heroku's documentation, and how to make the next missed run reach you.

Short answer

Check heroku logs --dyno scheduler (or your log add-on) for the run, and the Scheduler dashboard (heroku addons:open scheduler) for the job's frequency and next run time, which is in UTC for daily jobs. Then make the job prove it ran: give it a watch in Did It Run? and end the job with a request to the watch's ping URL. If the job is skipped or dies, no ping arrives and you get a missed alert at the scheduled time plus your grace window, from outside Heroku. Start free (3 watches, no credit card).

Check-ins are by ping URL today, with /start and /fail variants (/start marks a run as begun; it doesn't count as a finished run). Email check-ins are coming soon.

Why a Heroku Scheduler job doesn't run

  • Scheduler skipped it. Heroku's docs say Scheduler job execution "is expected but not guaranteed": in very rare instances a job may be skipped, and in very rare instances it may run twice. For critical jobs Heroku recommends a custom clock process instead (Heroku Dev Center, Heroku Scheduler). Either way, a skipped run doesn't alert you.
  • It ran at a different time than you think. The next run time for daily jobs is in UTC; Heroku says to add the UTC offset yourself if you want a local time (Heroku Scheduler, Scheduling jobs). A fixed UTC time also shifts by an hour against your local clock when daylight saving time starts or ends.
  • It was stopped for running longer than its interval. A dyno started by Scheduler won't run longer than its scheduling interval: a job that runs every 10 minutes is terminated after about 10 minutes. Heroku says anything that takes longer than a couple of minutes should run on a worker dyno (Heroku Scheduler, Long-running jobs).
  • Your Eco dyno hours ran out. Scheduler jobs run as one-off dynos, and on an Eco app a one-off dyno started by Scheduler consumes Eco dyno hours. When the account uses all its Eco dyno hours for the month, Heroku forces all Eco dynos on the account to sleep for the rest of the month (Heroku Dev Center, Eco Dyno Hours).
  • Another one-off dyno was using the slot. Heroku's limits allow 1 concurrent one-off Eco dyno per app (Heroku Dev Center, Limits). On an Eco app, a heroku run console left open, or a scheduled job that overlaps another, competes for that one slot.
  • It's a Container Registry app and the command isn't in the web image. With Container Registry deploys, the task must be accessible from the web image; there's no way to pick another image (Heroku Scheduler, Known issues).
  • The job ran, but failed. The error goes to your log stream as process scheduler.X. Nothing is emailed to you.

Step 1: read what Heroku recorded

  1. heroku logs --dyno scheduler shows your scheduled runs' log lines (Heroku Dev Center, Logging); Heroku's Scheduler article uses heroku logs --ps scheduler.1 for a single run. A run's lines go from "State changed from created to starting" to "State changed from up to complete" (Heroku Scheduler, Inspecting output). heroku ps shows recent scheduler dynos.
  2. Look soon. On Cedar-generation apps, Heroku's log router, Logplex, is designed for routing logs, not storing them, and keeps only the most recent 1,500 lines of your consolidated logs (Heroku Dev Center, Logging). On a busy app, last night's run can already be gone; a logging add-on keeps more. Fir-generation apps keep no log history at all, so there you need a telemetry drain (Logging, Log history limits).
  3. Open the Scheduler dashboard and check the job's frequency, dyno size and next run time, remembering that daily times are UTC.
  4. Run the same command with heroku run. Heroku says Scheduler uses the same one-off dynos, so if it works with heroku run, it will work from Scheduler (Heroku Scheduler, Testing tasks).
  5. On an Eco app, heroku ps also shows your remaining Eco dyno hours; so does the Billing page of your dashboard (Eco Dyno Hours). Heroku emails the account at 80% and 100% of the quota, so check for those emails too.

Step 2: add a heartbeat so a missed run reaches you

  1. Create a watch in Did It Run? with the same schedule as the job (in UTC, for a daily job), with a grace window that covers the job's normal run time.
  2. Store the ping URL as a config var (for example heroku config:set DIDITRUN_PING=<your watch's ping URL>) and read it inside a script, not on the Scheduler command line: Heroku expands config var values before logging the executed command, and says to avoid direct references to sensitive variables in task definitions (Heroku Scheduler, Defining tasks).
  3. Make the script check in when the job finishes, and call /fail when it errors. Then set the Scheduler task to bin/job-with-checkin:
bash
#!/usr/bin/env bash
# bin/job-with-checkin  (chmod +x, commit, deploy)
if bin/my-job; then
  curl -fsS -m 10 --retry 3 "$DIDITRUN_PING" > /dev/null
else
  status=$?
  curl -fsS -m 10 --retry 3 -X POST --data-binary "my-job exited with status $status" "$DIDITRUN_PING/fail" > /dev/null
  exit "$status"
fi

Now each case is covered:

  • The job didn't run (skipped by Scheduler, Eco hours used up, wrong UTC time): no ping arrives, so the run is missed at the deadline.
  • The job was terminated at its interval: it never reaches the success ping, so the run is missed.
  • The job ran and failed: it calls /fail and you get a failed alert. The alert shows the first 200 characters of the text you send.
  • The job ran, but on nothing: send a count (?items=$count) and set an output sanity check on the watch; see Scheduled job ran but output was empty.

Scheduler can occasionally run a job twice, so make the job safe to repeat. If the job could hang, also see Cron job or AI agent hung with no error.

Honest limits

  • Did It Run? doesn't connect to your Heroku account or read your logs. It only sees the check-ins your job sends.
  • A missed alert tells you the run didn't arrive, not why. Step 1 is how you find out which cause it was.
  • If the dyno can't reach the internet when the job finishes, the check-in can't arrive and the run shows up as missed, which is the safe way round.

What you'll see

  • Missed: no check-in by the expected time plus grace.
  • Late: an optional early warning while the run is still inside its grace window.
  • Failed: the job called /fail.
  • Empty: the check-in arrived but failed the watch's sanity check.
  • Recovered: the first good run after a problem.

Free covers 3 watches with email alerts; Solo ($15/month) adds Slack, Discord and Telegram alerts and a daily roll-call digest across all your automations.

FAQ

Does Heroku Scheduler guarantee my job runs?

No. Heroku's docs say Scheduler job execution is expected but not guaranteed: in very rare instances a job may be skipped or run twice. For critical jobs Heroku recommends running a custom clock process instead.

Why did my Heroku Scheduler job run at the wrong time?

The next run time for daily jobs is in UTC. Heroku says to add the proper UTC offset if you want the job at a certain local time.

Why was my Heroku Scheduler job stopped partway through?

A dyno started by Scheduler won't run longer than its scheduling interval, so a job scheduled every 10 minutes is terminated after about 10 minutes. Heroku recommends a worker dyno for anything longer than a couple of minutes.

Where are Heroku Scheduler logs?

In your app's log stream as process scheduler.X, for example heroku logs --dyno scheduler. On Cedar-generation apps, Heroku's log router keeps only the most recent 1,500 lines, so add a logging add-on if you need older runs.

Can Heroku Scheduler alert me when a job doesn't run?

Scheduler's docs don't describe any alert for a skipped run. An outside heartbeat watch that expects a check-in on schedule catches it, because a run that never happens sends nothing.

Can I check in by email instead of a URL?

Not yet. Email check-ins are coming soon. Today every check-in is an HTTP request to the watch's ping URL, with /start and /fail variants.

Know the next time it doesn't run

Add one ping URL to the end of your job. If the ping doesn't arrive on time, Did It Run? emails you.

Free for 3 watches. No credit card.

Start free: 3 watches