Disable a Heroku Scheduler Job Without Deleting It

Most of the time, your Heroku Scheduler jobs run in the background and you never think about them. Then you plan a data migration, and every job that touches that data has to stay off until you're done. A scheduled run in the middle of the migration would write to tables that are halfway through changing.

Heroku Scheduler has no way to pause a job. You can add, edit, or delete one, and deleting one means typing its command, frequency, dyno size, and next run time back in afterward, where a typo is easy to miss.

To disable a Heroku Scheduler job without deleting it, leave the job where it is and have it check a config var before it does anything. While the config var is set, each run logs that it's paused and exits. Unset it, and the next scheduled run works as before. This guide sets up that switch and takes you through one pause from start to finish.

Why maintenance mode and scaling don't pause a job

Maintenance mode looks like the obvious switch, but it doesn't reach Heroku Scheduler. The Dev Center article on maintenance mode says:

Scheduler jobs can still run while your app is in maintenance mode. Be mindful if you have any jobs set to run.

Scaling doesn't help either, because heroku ps:scale manages the dyno formation, and Heroku Scheduler runs are one-off dynos outside it.

Step 1: Add a pause switch to the job

The switch is a few lines at the top of each job that touches the data. A job without it keeps running through the migration, so add it to all of them, and deploy it ahead of time so the migration itself needs no deploy.

The job checks one config var, HEROKU_SCHEDULER_PAUSED, which pauses every job that has the switch. An unset config var means the job runs, so a missing one never stops a job by accident. The example is in Node.js, but the same check works in any language:

// bin/nightly-report.js: the Heroku Scheduler command is `node bin/nightly-report.js`
// The pause switch runs first, before the database connection or any other work.
if (process.env.HEROKU_SCHEDULER_PAUSED) {
  console.log('Heroku Scheduler is paused: HEROKU_SCHEDULER_PAUSED is set. Exiting without running.');
  process.exit(1);
}

const db = require('../lib/db');
// The job itself follows.

Any non-empty value pauses the job, even false, so you switch it back on by unsetting the config var, not by changing its value.

A paused run exits with status 1 on purpose. It then looks like a failed run, in your logs and in any monitoring that watches exit statuses, which makes the switch hard to forget. Alternatively, you can exit with 0, so paused runs count as successes, at the cost of that reminder.

You can use the same technique for specific jobs or groups of jobs, each with a config var of its own, such as HEROKU_SCHEDULER_PAUSED_NIGHTLY_REPORT. How granular to make it is up to you.

Step 2: Turn the switch on

With the switch deployed, turning it on is one heroku config:set command. Set it before the migration starts, on every app that runs the jobs, because staging and production each have their own config vars. Setting a config var creates a release, which also runs your release command if the app has one, so check that the release succeeded:

# Pause every job that has the switch
$ heroku config:set HEROKU_SCHEDULER_PAUSED=true --app example-app
Setting HEROKU_SCHEDULER_PAUSED and restarting ⬢ example-app... done, v42
HEROKU_SCHEDULER_PAUSED: true
# Check that the release succeeded
$ heroku releases --app example-app
=== example-app Releases - Current: v42
v42  Set HEROKU_SCHEDULER_PAUSED config vars  you@example.com  2026/10/19 09:58:12 +0000
v41  Deploy 7c35f77                        you@example.com  2026/10/18 16:20:45 +0000

Step 3: Stop the runs already in progress

Runs that started before you set the switch in Step 2 have already passed their pause check, so they continue until they exit. If they shouldn't keep going, stop them by name. Heroku Scheduler runs show up in heroku ps as scheduler.X, with a number for the X, so list the app's dynos and look for any that are still running:

# List the app's dynos, including running Heroku Scheduler runs
$ heroku ps --app example-app
=== run: one-off processes (1)
scheduler.1234 (Standard-1X): up 2026/10/19 09:55:10 +0000 (~ 4m ago): node bin/nightly-report.js

heroku ps:stop with --dyno-name stops that run and leaves everything else alone:

# Stop one running Heroku Scheduler run
$ heroku ps:stop --dyno-name scheduler.1234 --app example-app
Stopping dyno scheduler.1234 on ⬢ example-app... done

Step 4: Confirm the paused runs in your logs

To check the switch, watch the first run after the job's next run time. A paused run exits within seconds, so you won't catch it in heroku ps. Open your logs before the run starts instead: heroku logs with --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
2026-10-19T10:00:02.101234+00:00 heroku[scheduler.1234]: Starting process with command `node bin/nightly-report.js`
2026-10-19T10:00:02.601234+00:00 heroku[scheduler.1234]: State changed from starting to up
2026-10-19T10:00:03.201234+00:00 app[scheduler.1234]: Heroku Scheduler is paused: HEROKU_SCHEDULER_PAUSED is set. Exiting without running.
2026-10-19T10:00:03.401234+00:00 heroku[scheduler.1234]: Process exited with status 1
2026-10-19T10:00:03.501234+00:00 heroku[scheduler.1234]: State changed from up to complete

A paused run logs its pause line, and its Process exited with status 1 is the deliberate exit from Step 1.

A paused run still starts a dyno, so you pay for the seconds it runs: usage is wall-clock time, prorated to the second.

Step 5: Switch the jobs back on

When the migration is done, unset the config var with heroku config:unset. The jobs in Heroku Scheduler never changed, so there's nothing to set up again.

# Switch the jobs back on
$ heroku config:unset HEROKU_SCHEDULER_PAUSED --app example-app
Unsetting HEROKU_SCHEDULER_PAUSED and restarting ⬢ example-app... done, v43

Unsetting creates a release too, so your release command runs again if the app has one.

If you'd rather have a paused state in the scheduler itself, our Advanced Scheduler add-on has an active or inactive state for each trigger, with triggers:deactivate in its Heroku CLI plugin.

Get reminded while jobs are paused

A switch that's easy to turn on is also easy to leave on, and Heroku Scheduler Monitor can do the remembering for you. It monitors failures, interruptions, and more, and because a paused run exits with status 1, it emails you each time one runs, until you switch the job back on. A run you stopped in Step 3 is a forced exit, so it counts as an interruption, and you get an email about that too. The trade-off is that paused runs show up as failures in its data while the switch is on.

Heroku Scheduler Monitor 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

Your jobs now stay in Heroku Scheduler while they're switched off, and come back with one command. Once they're back on, run the job manually before its next scheduled run to check it works after the migration.

Originally published:

  • Heroku Scheduler
  • Background jobs
  • Alerts

Get more out of Heroku Scheduler

Install the add-on and start seeing data the moment Heroku Scheduler runs your next scheduled job.

Install from ElementsExplore Pricing