How to Run a Heroku Scheduler Job Manually
You've changed a Heroku Scheduler job and want to see it work now, not at its next run. Heroku Scheduler has no Run button, though, and even a temporary copy of the job on the shortest frequency, every 10 minutes, can leave you waiting up to 10 minutes for every test. It's quicker to start a one-off dyno with the job's command yourself, using heroku run.
But there's a catch: a scheduled run is stopped when it reaches its interval, and a run you start with heroku run isn't, so it can keep running for up to 24 hours. A job that has grown too slow passes your test, then gets cut short on its next scheduled run.
To run a Heroku Scheduler job manually and trust the result, start the job's exact command in a detached one-off dyno, with the job's dyno size and the same time limit a scheduled run gets. The Heroku CLI covers the command and the dyno size but doesn't set a time limit. Fortunately, the Platform API can. This guide uses both, then shows how to read the result and what a manual run still can't tell you.
Before you start
You need:
- The Heroku CLI, logged in to an account with access to the app.
curl, or any other HTTP client, for the Platform API step.- An app with a job in Heroku Scheduler.
What Heroku Scheduler sets on every run
Heroku Scheduler runs each job in a detached one-off dyno with the job's command and dyno size, and your app logs show the run as process scheduler.X. The setting most tests miss is the time limit: Heroku Scheduler creates the dyno with a time to live (TTL) equal to the job's interval. When the TTL runs out, the run is forcibly stopped: Heroku sends the process a SIGTERM, then a SIGKILL 30 seconds later if it's still running.
A manual run can match each of these settings, though not with the Heroku CLI alone:
| Setting | A scheduled run | Heroku CLI | Platform API |
|---|---|---|---|
| Connection | Detached: output goes to your app logs | heroku run:detached |
"attach": false |
| Command | The job's command, or a process type with its parameters | The same command (Step 1) | The same command |
| Dyno size | The size you picked for the job | --size, set to that size |
"size", set to that size |
| Time limit | A TTL equal to the job's interval | No flag for it | "time_to_live", set to that number of seconds |
| Dyno name | scheduler.X, with a number for the X |
run.X |
run.X, from "type": "run" |
The name always differs, and that's deliberate: your test is never mistaken for a scheduled run. It also means log searches and monitoring that filter on scheduler.X don't see it.
Step 1: Copy the job's command, frequency, and dyno size
Open the Heroku Scheduler dashboard and copy the job's command, frequency, and dyno size exactly as they appear:
# Open the Heroku Scheduler dashboard in your browser
$ heroku addons:open scheduler --app example-app
If the job uses a process type instead of a command, Heroku Scheduler runs that process type's command from your Procfile, plus the job's parameters. Copy the command from your Procfile and add the parameters to it.
Check the job's next run time as well, and start your manual run well away from it. Otherwise your test and the scheduled run can work on the same data at once. Both also count toward your app's limit on concurrent one-off dynos, which is a single dyno for Eco.
Step 2: Start the job with the Heroku CLI
Start the job's command with heroku run:detached, which needs no token, and pass the job's dyno size with --size:
# Start the job's command in a detached one-off dyno
$ heroku run:detached "bin/nightly-report" --size standard-1X --app example-app
Running bin/nightly-report on ⬢ example-app... done, run.1234 (Standard-1X)
Run heroku logs --app example-app --dyno run.1234 to view the output.
Besides the name, the difference that matters is the time limit. heroku run:detached has no flag for a TTL, and the CLI's source code sends none, so the run can keep going for up to 24 hours, the longest a one-off dyno can run. A job that outlasts its interval still passes this test.
Plain heroku run is further off still, even though it's what the Heroku Scheduler article uses for testing tasks. It sets no TTL either, and it ends when you press Ctrl+C, when you disconnect, or after a period of inactivity. Its output also goes to your terminal, so your app logs record only the dyno's startup and shutdown.
If the job finishes well inside its interval, the time limit never comes into play and this test is close enough, so you can skip ahead to Step 4. If the job can run close to its interval, Step 3 adds the limit.
Step 3: Match the time limit with the Platform API
The Platform API lets you set the TTL that the Heroku CLI leaves out, so your manual run stops where a scheduled one would. The Dyno Create endpoint takes a TTL as time_to_live, in seconds, up to 86,400 (24 hours). Set it to the job's interval, and always send it, because without it the run gets the same 24 hours as in Step 2:
| Job frequency | time_to_live |
|---|---|
| Every 10 minutes | 600 |
| Every hour | 3600 |
| Every day | 86400 |
Then send the request. It starts the same detached run as in Step 2, with the TTL added, so put the job's command and dyno size in place of bin/nightly-report and standard-1X. The Platform API takes a bearer token, and heroku auth:token prints the one your Heroku CLI uses:
# Start the job's command the way Heroku Scheduler would, with a 10-minute TTL
$ curl -X POST https://api.heroku.com/apps/example-app/dynos \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $(heroku auth:token)" \
-d '{"command": "bin/nightly-report", "attach": false, "size": "standard-1X", "type": "run", "time_to_live": 600}'
{
"attach_url": null,
"command": "bin/nightly-report",
"created_at": "2026-10-19T10:00:01Z",
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "run.1234",
"app": {
"id": "fedcba98-7654-3210-fedc-ba9876543210",
"name": "example-app"
},
"release": {
"id": "89abcdef-0123-4567-89ab-cdef01234567",
"version": 41
},
"size": "standard-1X",
"state": "starting",
"type": "run",
"updated_at": "2026-10-19T10:00:01Z"
}
Step 4: Follow the run in your logs and read its exit status
Watch the run live. Heroku keeps only the last 1,500 lines of log history, and none at all for apps in Shield spaces with Private Space Logging enabled, so a run you don't follow may be gone by the time you look. heroku logs with --dyno-name and --tail shows only that dyno's lines as they arrive. Put the dyno name from Step 2 or Step 3 in place of run.1234:
# Follow the manual run's log lines
$ heroku logs --dyno-name run.1234 --tail --app example-app
2026-10-19T10:00:02.101234+00:00 heroku[run.1234]: Starting process with command `bin/nightly-report`
2026-10-19T10:00:02.601234+00:00 heroku[run.1234]: State changed from starting to up
2026-10-19T10:10:02.201234+00:00 heroku[run.1234]: Cycling
2026-10-19T10:10:02.211234+00:00 heroku[run.1234]: State changed from up to complete
2026-10-19T10:10:02.221234+00:00 heroku[run.1234]: Stopping all processes with SIGTERM
2026-10-19T10:10:02.801234+00:00 heroku[run.1234]: Process exited with status 143
This example is the run from Step 3. The job is still working when its 600-second TTL runs out, the same way a slow job outlasts its interval.
The last line gives the result. On the Common Runtime, Process exited with status 0 means the job succeeded, and any other status means it didn't finish cleanly. A run stopped at its TTL ends with Stopping all processes with SIGTERM and Process exited with status 143, exactly as a scheduled run does at its interval. If yours ends that way, you have three options: rework the job so it finishes inside its interval, move work that takes longer than a couple of minutes to a worker dyno, or, if the job doesn't need to run that often, switch it to a longer frequency, such as hourly instead of every 10 minutes, or daily instead of hourly.
What running a Heroku Scheduler job manually can't test
A run that matches every setting still can't reproduce the time of day. It starts when you start it, not at the job's scheduled time, which for a daily job is set in UTC. Data or work the job depends on, such as an import that runs earlier in the night, may not be ready yet when you test.
Where Heroku Scheduler Monitor fits
A manual run tells you the job works once. Heroku Scheduler Monitor keeps watch on the scheduled runs after that. It monitors failures, interruptions, and more, so it emails you when a job fails, and when a run reaches its interval and Heroku forcibly stops it. That lets you step in and fix a job as soon as it goes wrong.
Heroku Scheduler Monitor only considers the detached scheduler one-off dynos that Heroku Scheduler starts, so your manual runs stay out of its data. It starts working for you the moment you install it, with nothing to configure: no config var, code change, or redeploy. You can find it in the Heroku Elements Marketplace.
Next steps
You can now reproduce a Heroku Scheduler job and trigger it manually, with the same command, dyno size, and time limit as a scheduled run. To keep a job from running while you fix it, switch it off without deleting it.
Originally published:
