We Were Wrong: Don't Use Heroku Scheduler

Adam McCrea

Adam McCrea

@adamlogic

After seven years, we’re finally ready to say goodbye to Heroku Scheduler. It has served us well enough—there’s no hate here. But we won’t be using it in future projects, and we don’t recommend you do, either. This is the story of why we’re switching, what we’re switching to, and an exploration of alternatives.

What is Heroku Scheduler?

Heroku Scheduler is a free Heroku add-on (provided by Heroku) for scheduling recurring tasks or jobs. You do it through a web UI that looks like this:

Heroku Scheduler with several jobs

Scheduler lets you add an unlimited number of jobs that repeat on a schedule of every 10 minutes, every hour, or daily. We’ve been using it at Judoscale for seven years. We started with it because it was free and super simple, and indeed, it’s very easy to use and has been super reliable for us.

So what's the problem?

Every 10 minutes is not good enough

I generally have a mindset of embracing constraints as much as possible. I’m a Ruby on Rails developer who fell in love with “convention over configuration”, happily rolling with framework defaults whenever possible. Constraints and conventions free our minds to focus on creating value for our customers, rather than bike-shedding solved problems.

For years, I’ve taken the same view of Heroku Scheduler. In other words, I’ve let the constraints of the product dictate how I architect my scheduled jobs, instead of vice versa. “Every 10 minutes is good enough!”, I would say.

But it’s not good enough.

The core of our product is an autoscaler that needs to run every 10 seconds (not minutes). We also support scaling on a schedule, and those schedules need to be resolved every minute.

For years, we’ve hacked around this by having some of our jobs re-enqueue themselves instead of scheduling them with a job scheduler. This is a brittle system! If a Sidekiq process is terminated in an unsafe way, the job will stop running. Ask me how I know!

Fortunately, we have very good monitoring and alerting in place, so it's been a quick fix the few times this has happened. But even a few minutes of downtime is unacceptable for a core part of our product.

But it's not just that.

Your job schedule should be version-controlled

We did use Heroku Scheduler for many other recurring tasks. We have jobs that run nightly for things like calculating daily usage for our customers, reconciling our systems with our platform partners, and sending notifications. Heroku Scheduler has worked really well for this!

But I can’t tell you the history of our job schedule, and I can’t guarantee that our staging and production schedules are in sync. Heroku Scheduler is managed via a web UI, not code, so nothing is version-controlled, and nothing is automated when we deploy.

This hasn’t caused any major problems for us, but it’s annoying, and it’s just a matter of time until we forget to schedule an important job after a deploy. Our workaround is to add notes in our pull requests so we don't forget to schedule the appropriate jobs in staging and production.

Screenshot of a GitHub pull request with tasks to set the schedule in staging and production

It’s time for our job scheduler—and our workflow around it—to grow up!

Alternatives to Heroku Scheduler

There are many ways to run recurring, scheduled jobs, and there’s no one clear replacement for Heroku Scheduler. The alternatives fall into four categories:

  • Good ol’ Cron
  • Other add-ons and third-party services
  • Framework-integrated job schedulers
  • Dedicated clock processes

Cron vs. Heroku Scheduler

Cron is the classic solution to schedule tasks, as it’s built into Linux and has existed for decades. It’s very simple: you manage a “crontab” configuration file, and Cron runs your jobs for you.

Example Crontab configuration file

Cron is not an option on Heroku, though, because Heroku dynos are ephemeral and stateless. The schedule also lives outside of the app source, so it's not version-controlled in the way we'd like.

And so we move on…

Other scheduling add-ons for Heroku

Heroku Scheduler is the free add-on provided by Heroku, but there are a few third-party add-ons that are more capable, notably Cron To Go and Advanced Scheduler. These services do solve the problem of Heroku Scheduler’s limited scheduling options, but they don’t solve the problem of version-controlling our schedule—that’s still handled via a web UI.

Screenshot of Heroku scheduling add-ons

Let’s keep exploring…

Integrating a scheduler with our background job system

Many background job systems—in our case, Sidekiq—have a mechanism for scheduling recurring jobs. The open source version of Sidekiq doesn’t handle this natively, but Sidekiq Enterprise does, and there are several third-party extensions to Sidekiq that add scheduling capability, the most popular being sidekiq-scheduler and sidekiq-cron. Other background job processors like Good Job, Delayed Job, and Celery have similar extensions.

One significant constraint with this approach is that each scheduled job must be written as a background job—for us that means only Sidekiq jobs can be enqueued on a schedule. This is actually a good thing! Sidekiq (like most background job systems) has excellent support for error handling, retries, prioritization, and scaling. By keeping the actual work of our scheduled jobs in Sidekiq, we get all those benefits for free. The scheduler itself should be as lightweight as possible so it doesn’t need to scale and minimizes the opportunities for errors.

Even with Heroku Scheduler, we were already embracing this separation of scheduling and execution, where Heroku Scheduler handled the scheduling, but Sidekiq did the actual work (execution).

Screenshot of Heroku Scheduler enqueueing a Sidekiq job

Embedding a job scheduler with a background job processor isn’t without its quirks, though. The scheduler will run in every Sidekiq process, and we’re always running several worker dynos. There are some workarounds here, but they’re not pretty. Sidekiq Periodic Jobs (in Sidekiq Enterprise) handles this automatically by electing a “leader” process that handles the scheduling, but we’re not currently using Sidekiq Enterprise, so it’s not yet an option for us.

Beyond that, there's a bit a "messiness" in having the scheduler embedded in the background job processor. If we encounter issue of scale or resource usage, there's no way to monitor the scheduler on its own.

These are pretty minor concerns, though. Using an embedded scheduler is a viable option, especially since it leverages tools we're already using.

Let’s take a look at one more option…

Using a dedicated clock process

A “clock process” is an executable program that runs indefinitely (a daemon) whose sole responsibility is triggering scheduled tasks. It’s conceptually similar to Cron, but written as an executable that lives in your codebase. The clock process is run in an isolated environment—in Heroku-land, that means it gets its own line in a Procfile.

Screenshot of a Procfile with a clock process defined

In our case, we’re running a Ruby on Rails application, so we might reach for something like clockwork or ruby-clock. Both of these tools define the job schedule in code, so it’s version-controlled (yay!). The scheduling itself can use Cron syntax or natural language, with the ability to run as frequently as every second (double-yay!!).

The only real downside of this approach is the extra cost of running a dedicated dyno for the clock process. We’re at a scale where that extra cost is negligible, but it might be a concern for a new app or a hobby project.

So where did we land?

Running a dedicated clock process ultimately felt like the cleanest approach here. Conceptually, we like the clear separation between “scheduling” and “executing”. These are two different responsibilities, and it makes sense for them to run as separate processes.

We chose ruby-clock for our clock process. A few reasons:

  • The scheduling DSL is super simple and does exactly what we need.
  • The README is clear and concise.
  • It has substantially fewer lines of code than clockwork (the most similar competing tool).
  • The maintainer appears to be responsive to issues.

Ruby-clock was a breeze to set up, and we now have a version-controlled job schedule that’s much more flexible than Heroku Scheduler.

Screenshot of our ruby-clock schedule

Living without Heroku Scheduler

We’re a couple weeks into our migration from Heroku Scheduler to ruby-clock, and we couldn’t be happier with the change. Our scheduled jobs are running flawlessly, and our schedule is a readable, version-controlled work of art.

Personally, I won’t be using Heroku Scheduler on future projects. It does its job just fine, but there are plenty of other options (many of them free) that do the job better. They’re more maintainable, more flexible, and they make me a happier dev.