Heroku Scheduler Timeout: Why Your Job Stops at Its Interval
Heroku Scheduler is made for short jobs. You give it a command and a frequency, and every 10 minutes, every hour, or every day it starts a one-off dyno to run that command. For most jobs, that's all you ever need to know about it.
The trouble starts when a job grows. A report that used to finish in a few minutes now needs longer, and one morning the numbers are only half there. There's no error from your code, only Process exited with status 143 at the end of the run's log.
That's the Heroku Scheduler timeout. Each run gets as long as its interval and no longer, so Heroku stops a job on the 10-minute frequency after about 10 minutes, wherever it is in its work. This post shows how to confirm that's what happened, and three ways to keep it from happening again.
What you see in the logs
On the Common Runtime, a run cut off at its interval ends with these four lines, as in this Dev Center example of an interrupted one-off dyno:
Cycling
State changed from up to complete
Stopping all processes with SIGTERM
Process exited with status 143
You find them in your app logs, where each Heroku Scheduler run appears under the dyno name scheduler.X, with a number for the X.
Why the Heroku Scheduler timeout equals the interval
Heroku Scheduler gives each run's dyno a time to live (TTL) equal to the job's interval. Once the TTL runs out, Heroku forcibly stops the dyno: it sends the process a SIGTERM, then a SIGKILL 30 seconds later if it's still running. The Heroku Scheduler article states the limit directly:
A dyno started by scheduler will not run longer than its scheduling interval. For example, for a job that runs every 10 minutes, dynos will be terminated after running for approximately 10 minutes.
So the limit moves with the frequency you picked for the job:
| Job frequency | The run stops after |
|---|---|
| Every 10 minutes | About 10 minutes |
| Every hour | About 1 hour |
| Every day | About 24 hours |
Each limit is approximate: Heroku stops the dyno with some jitter, so two runs of the same job can overlap for a moment.
How to confirm it was the interval
A run cut off by its interval lasted about one interval and ended with those SIGTERM lines, so you need two things: the job's frequency and the run's log. The frequency is in the Heroku Scheduler dashboard:
# Open the Heroku Scheduler dashboard in your browser
$ heroku addons:open scheduler --app example-app
Then pull up the log lines of the run that stopped, using its dyno name instead of scheduler.1234:
# Show the log lines of one Scheduler run
$ heroku logs --dyno-name scheduler.1234 --app example-app
2026-10-19T10:00:02.101234+00:00 heroku[scheduler.1234]: Starting process with command `sleep 900`
2026-10-19T10:00:02.601234+00:00 heroku[scheduler.1234]: State changed from starting to up
2026-10-19T10:10:02.201234+00:00 heroku[scheduler.1234]: Cycling
2026-10-19T10:10:02.211234+00:00 heroku[scheduler.1234]: State changed from up to complete
2026-10-19T10:10:02.221234+00:00 heroku[scheduler.1234]: Stopping all processes with SIGTERM
2026-10-19T10:10:02.801234+00:00 heroku[scheduler.1234]: Process exited with status 143
This example is a job that runs sleep 900 on the every-10-minutes frequency, so it outlasts its interval. Compare the time of the run's first line with the time of Stopping all processes with SIGTERM. If they sit about one interval apart, the interval ended the job. If the run ended well before that, something else ended it, and the job's own output in the same log has the error.
If the run happened overnight, its lines may already be gone, because Heroku keeps only the last 1,500 lines of log history. In that case, follow the next run live: --tail streams log lines as they arrive, and --process-type scheduler limits them to Heroku Scheduler runs.
# Stream the log lines of every Heroku Scheduler run
$ heroku logs --tail --process-type scheduler --app example-app
How to fix a job that outlasts its interval
You have three ways out: make each run smaller, hand the work to a worker dyno, or give the job a longer interval. The Heroku Scheduler article points to the first two, because scheduled jobs "are meant to execute short running tasks or enqueue longer running tasks into a background job queue". Which one fits depends on the work.
Split the work so each run finishes in time
If the job works through a list (records to export, rows to sync, files to process), give each run a bounded batch, record where it stopped, and let the next run pick up from there. Size each batch to finish in a couple of minutes, well inside the interval. Because of the jitter in stop times, make the job safe to overlap with the previous run, for example with a lock or idempotent writes.
Move long work to a worker dyno
When one piece of work takes longer than the interval, or the job is long by design, take it out of Heroku Scheduler and give it to a worker dyno, which reads work from a background job queue and has no interval to stop it. Declare the worker in your Procfile, with your own worker script as bin/report-worker:
worker: bin/report-worker
Then scale it up:
# Start one worker dyno from the Procfile entry
$ heroku ps:scale worker=1 --app example-app
Scaling dynos... done, now running worker at 1:Standard-1X
Finally, change the Heroku Scheduler job to a command that only enqueues the work, such as your own bin/enqueue-nightly-report script.
The trade-off is cost. Heroku computes dyno usage from wall-clock time, so the worker accrues usage for as long as it's scaled up, not only while it works.
Run the job less often
If the work has to happen in one piece and can wait longer between runs, give it a longer interval. The limit moves with it, as the table in the previous section shows, and the price is fewer runs. A daily job's next run time is in UTC, so convert your local time when you pick it.
If you want a longer limit without running less often, our Advanced Scheduler add-on sets a timeout per trigger, separate from the schedule, up to 86,400 seconds (24 hours).
Catch it the next time it happens
Heroku Scheduler Monitor emails you when a Heroku Scheduler run is cut off. It monitors failures, interruptions, and more, and it counts any run that's forced to exit, instead of exiting on its own, as an interruption. So you hear about a run stopped at its interval right away, instead of finding half a report the next morning.
Heroku Scheduler Monitor starts working for you the moment you install it, with nothing to configure: no config var, code change, or redeploy. It reports interruptions; it doesn't extend the interval or rerun the job. You can find it in the Heroku Elements Marketplace.
Next steps
You can now tell a run cut off by its interval from one that failed. For the other ways a Heroku Scheduler run can end badly, read the Dev Center section on troubleshooting one-off dyno issues.
Originally published:
