Open-source alternative to Heroku Scheduler: which option fits your scheduled jobs
For a single-server app, Dokku cron tasks defined in `app.json` are the closest replacement for Heroku Scheduler, and for a multi-node cluster, Kubernetes CronJobs are the natural fit. A custom clock process, a long-running worker that wakes on a schedule and enqueues work, sits between the two and suits teams that want scheduling logic in the application. Heroku's own Scheduler documentation recommends a custom clock process for critical scheduled jobs, which is a useful signal that best-effort execution is the wrong default for anything your business depends on. Scheduled jobs on Heroku Scheduler run in one-off dynos, may be skipped or run twice in rare cases, and are terminated once they exceed their interval, so the first inventory task is to list every job, its interval, its runtime and what breaks if it misses a run.
Once you have that list, decide by where the jobs will run. Dokku's cron tasks are declared in the same app.json format Heroku uses, and they run on the Dokku host, so they stay simple but inherit that host's failure domain. Kubernetes CronJobs add a concurrencyPolicy, startingDeadlineSeconds and retry history, but they only pay off if you already operate a cluster, and the control plane, etcd backups and Secrets handling become your responsibility. Confirm with your platform team how overlapping runs, missed windows and job history should be handled for each job, and write those rules down before you migrate, because the answers differ between a nightly report and a payment reconciliation. Also confirm how job logs and failure alerts reach your monitoring stack, since neither option gives you Heroku's dashboard by default.
Heroku Scheduler capabilities mapped to open-source options
This table maps each Heroku Scheduler behavior to the Dokku cron task, Kubernetes CronJob, clock process and external monitoring options that replace it. Read each row as a question to answer in your own environment, because Heroku's guarantees are best-effort and open-source options vary by version and setup. The main takeaway is that the reliability and alerting you get from Heroku must be designed in, and several rows need confirmation before you rely on them.
| Capability | Dokku cron tasks | Kubernetes CronJobs | Clock process and external monitoring |
|---|---|---|---|
| Intervals (10 minutes, hourly, daily) | Cron expressions defined in app.json or set per app, so any schedule the cron syntax supports is possible | Cron schedule field in the CronJob spec, with any cron expression the controller accepts | Clock process loops on its own timer, so the interval is code you own and test |
| One-off run per execution | Each cron task runs as a separate container from the app image, similar to a one-off dyno | Each run creates a Job and Pod, so runs are isolated and can be inspected | Clock process usually spawns work in-process or through a queue, so isolation depends on your design |
| Overrun handling (job longer than interval) | Confirm overlap behavior for your version, and add a lock such as a file or database lock in the job | concurrencyPolicy can forbid overlap, replace or allow runs; confirm the setting you choose | Nothing enforces this by default, so implement a lock or lease and a timeout inside the job |
| Skipped or duplicate runs | Not guaranteed by the scheduler; make jobs idempotent and record a run ID | startingDeadlineSeconds and missed-run limits affect skipped runs; confirm behavior for your cluster | Use a database unique key per scheduled window so a duplicate run is rejected |
| Run history and logs | Logs available through dokku logs and your log drain; confirm retention on the host | Job and Pod objects keep history up to a configured limit; logs come from your logging stack | Ship logs to a self-hosted stack such as Grafana Loki or OpenSearch; this is your responsibility |
| Manual (on-demand) runs | Run the task command with dokku run in the app context | Create a Job from the CronJob template with kubectl create job | Expose a protected endpoint or CLI command that triggers the same code path |
| Failure alerts | No built-in alerting for failed cron runs; route output to your alerting tool | Job failure metrics are exposed; alerting needs Prometheus or equivalent rules | Dead-man alerting, where silence triggers an alert, requires an external monitor such as a heartbeat check |
| Operator effort | One host to patch and back up; the scheduler runs on the same server as the app | Control plane and node upkeep, etcd backups and access control for Secrets used by jobs | Monitoring service, heartbeat endpoint and alert routing to maintain |
| What to confirm before cutover | Cron task behavior and log retention in your Dokku version | Concurrency, deadline and history settings in your cluster version | Heartbeat tool, alert channels and who receives pages at night |
Choosing Dokku, a clock process or Kubernetes by environment
Dokku is an extensible, open source platform as a service that runs on a single server, so scheduled work lives on the same host as the app. You define recurring tasks as cron entries in the app's app.json manifest, which follows Heroku's format, and Dokku runs them as scheduled processes in the app's container. Dokku's default docker-local scheduler runs containers directly on the Dokku host, which keeps the setup simple but means a failed host stops both the web app and its jobs.
- Schedule definition: cron tasks are declared in
app.json, and Dokku documents that not every Heroku manifest feature is supported, so confirm each entry after the first deploy. - Overrun and failure: a long run can overlap the next one unless your script guards against it with a lock, and a host outage halts every job until the server returns.
- Operator duties: you patch the host, monitor disk and memory (Dokku needs at least 1 GB of RAM for the default Docker scheduler), and keep the cron logs.
- Single-server limit: the default scheduler does not spread jobs across machines, so capacity and job isolation are bounded by that one server.
- Multi-server option: the k3s scheduler, introduced in Dokku 0.33.0, builds a multi-server K3s cluster but requires a Docker registry, and only the initial Dokku server is configured as the git push target.
Use this model for small teams with a few low-risk jobs. Confirm with Dokku's scheduler documentation which scheduler your release uses before you design failure handling.
A clock process is a long-running worker process that sleeps, wakes on a fixed interval and enqueues or runs work. It works on any platform that can run a second process type, including Dokku, Coolify and Heroku itself. Heroku's own Scheduler documentation recommends a custom clock process for critical scheduled jobs, which signals the trade-off: you gain control over timing and error handling in exchange for owning the loop.
- Schedule definition: the interval and logic live in application code, so changes ship through the normal deploy, and the schedule is versioned with the app.
- Overrun and failure: your code decides what happens if a job runs past its interval, whether it skips, queues or runs twice, which removes the best-effort ambiguity of a managed scheduler.
- Operator duties: you must keep the clock process running, restart it on crash, and alert when it stops, because a silent clock process means silent missed work.
- Idempotency: design each job to be safe to run twice, since retries and restarts can repeat a run.
- Process model: on Dokku, add the clock as a separate process type in the Procfile and scale it with
ps:scale, so it can be restarted independently of the web process.
Choose this model when you need a schedule that is tied to application logic or when you want the same mechanism on any host. Verify the restart policy and alerting in your own environment before relying on it for billing or compliance jobs.
Kubernetes runs scheduled work as CronJobs, which create Jobs on a timetable across the nodes in a cluster. The cluster, rather than one host, decides where each run goes, which removes the single-server dependency. Kubernetes is open source under Apache 2.0, and its three most recent minor releases are maintained, so plan an upgrade path before you adopt it.
- Schedule definition: each CronJob is a manifest with a cron expression, and your team manages it through version control and the same review process as the application.
- Overrun and failure: a CronJob can be configured to forbid overlapping runs or replace a running one, and the Job controller reschedules work when a node fails, so confirm these settings for each job.
- Operator duties: you run the control plane, which the kubeadm high availability guide describes with at least three control plane nodes and a TCP load balancer in front of kube-apiserver, and you back up etcd with a tested restore procedure.
- Secrets and access: Secrets are stored unencrypted in etcd by default, so configure encryption at rest and least-privilege RBAC before storing job credentials.
- Upgrade cadence: Kubernetes ships about three minor releases a year, and the version skew policy sets the order in which control plane and node components must be upgraded.
Choose this model when jobs must survive node loss or when the platform already runs on Kubernetes. Ask your platform team to confirm the cluster's control plane design, etcd backup status and CronJob concurrency policy.
Four ways to replace Heroku Scheduler and when each fits
Heroku Scheduler is a convenient add-on, but its execution is best effort and its jobs run in one-off dynos. The four options below move scheduling onto infrastructure you control, and each one shifts a different share of overlap handling, observability and scheduler upkeep onto your team.
Dokku app.json cron tasks
Dokku reads an app.json manifest based on Heroku's format, and its cron tasks let a Heroku-style schedule carry over with little rewriting. This fits small teams running a single Dokku host who want the smallest change from Heroku Scheduler. The trade-off is that the schedule lives in the same single-server platform as the app, so a host failure stops both. Confirm how Dokku handles a run that overlaps the next scheduled start, where its output is logged, and which app.json keys are supported, because the docs note that not all Heroku functionality carries over.
Dedicated clock process
A clock process is a long-running worker that uses an in-process scheduling library to trigger jobs, and it is the pattern Heroku's own documentation recommends for critical work. It fits teams that need custom logic, such as skipping a run when a lock is held or retrying with backoff. You own the scheduler code, its restarts and its locking, so a second instance can trigger duplicate jobs unless only one clock runs. Check that your library documents its behavior across multiple processes and that your platform keeps exactly one clock replica running.
Kubernetes CronJobs
Kubernetes CronJobs create a Job on a cron schedule, and their concurrency policy controls whether a new run may start while an earlier one is still active. This fits teams already running a cluster for other workloads and needing retries, resource limits and history of past runs. The cost is operating the cluster itself, including control plane upkeep, etcd backups and upgrades. Confirm the concurrency and history settings in your cluster version, how missed start windows are handled, and where job logs are shipped, because pod logs are removed with the pod.
External scheduler calling an HTTP endpoint
An external scheduler, such as a hosted cron service or a self-hosted tool, calls a URL in your app on a timetable, and the job itself runs inside the app. This fits teams whose work is already exposed as an authenticated endpoint and who want the scheduler outside the app host. The trade-off is that the endpoint must be idempotent, protected by a secret, and bounded in duration, and the scheduler becomes another system to monitor. Confirm whether the scheduler records each call's status, retries failed calls, and whether it can alert on a missed run.
Making scheduled jobs reliable and observable after the move
Heroku Scheduler's documentation says execution is expected but not guaranteed, and that jobs can occasionally be skipped or run twice. After the move, that best-effort behavior becomes your problem to design around. Start by making every job idempotent, so a duplicate run produces the same result as a single run. Then add a lock, such as a row in PostgreSQL or a file lock on the host, so a slow run cannot overlap with the next scheduled start. Heroku's scheduled dynos were terminated once they outran their interval; on Dokku or Kubernetes nothing stops a hung job unless you set one. Put an explicit timeout on every job, for example the timeout command in the cron line or activeDeadlineSeconds on a Kubernetes CronJob, and decide whether a timed-out run should retry.
Failures will not show up in a dashboard by default. Route non-zero exit codes and timeouts to an alert channel your team reads, and make the job's own logs survive the container: confirm how long Dokku keeps container logs and where the job output goes, then set retention for the log store you choose. Schedule a dead-man check as well: each successful run pings an external URL, and an alert fires when the ping stops arriving. This catches the case no job-level alert can, where the scheduler itself stops firing. Confirm the tooling choices in your own environment. OpenTelemetry-based monitoring, with Prometheus, Grafana and Jaeger as the usual companions, can carry job metrics and traces, but the exporters, alert rules and storage sizing are decisions to make and test before cutover.
Moving scheduled jobs from Heroku Scheduler without losing runs
This sequence moves each Scheduler job to a new scheduler and proves the new runs match the old ones before the Heroku job is turned off, so no job is dropped during the cutover.
Inventory every scheduled job
Export the job list from the Heroku Scheduler dashboard or the Heroku CLI for each app, and record the command, the interval (every 10 minutes, hourly or daily), the dyno size and the owner of each job. Note which jobs write to a database, send email or call a third-party API, because those jobs need the most care during the move. Also list the config vars each command reads. The inventory is done when every job has a row in a shared table and someone on the team has signed off on it.
Confirm environment and database access
Copy the config vars that each job reads into the new Dokku app or Kubernetes Secret, and check the values against the Heroku originals rather than assuming they match. Confirm that the new runtime can reach the database and any external services, including firewall rules, allowed IP lists and TLS settings. Run each command once by hand in the new environment with a read-only or staging database if one is available. The step is complete when each job connects and produces the expected output without writing to production data.
Make each job idempotent
Heroku's documentation says a job may occasionally be skipped or run twice, so each job must produce the same result if it runs again for the same period. Add a run key based on the scheduled time, use upserts or unique constraints, and record a completion marker before sending any external notification. Jobs that cannot be made idempotent should stay on the old schedule until they can. The step is complete when a second manual run of each job leaves the data unchanged.
Run old and new schedules in parallel
Add the same jobs to the new scheduler, which can be Dokku's
app.jsoncron tasks or Kubernetes CronJobs, and keep the Heroku Scheduler jobs enabled. The idempotent design means both schedules can run without duplicating side effects. Keep the parallel period long enough to cover each job's interval and at least one full cycle of any daily or weekly work. The step is complete when the new schedule has run every job at its expected time.Verify logs, outputs and timing
Compare the logs from both schedules for each job, checking the start time, end time, exit code and the business output such as row counts, files or messages sent. Look for missed runs, duplicate runs and jobs that exceed their interval, since Heroku ends a scheduled dyno that runs longer than its interval. Set alerts on non-zero exit codes and on runs that do not finish, and confirm they reach a person. The step is complete when the outputs match for a full observation window and the alerts have been tested.
Disable the Heroku Scheduler jobs
Disable each job in Heroku Scheduler one at a time, starting with the least critical, and watch the new schedule for the next run of that job. Keep a written record of the disable time and who made the change. If a job's output changes unexpectedly, re-enable it in Heroku Scheduler and investigate. The step is complete when every job is disabled on Heroku and the new scheduler has carried the work for at least one full cycle without a gap.
Remove the Scheduler add-on and document the jobs
Remove the Heroku Scheduler add-on from each app once the old jobs are disabled, and update the runbook with the new schedule definitions, the alert destinations and the owner of each job. Store the cron definitions and the configuration in version control so future changes go through review. The step is complete when the add-on no longer appears on any app, and the runbook lists every job with its owner and check.
Questions buyers ask before replacing Heroku Scheduler
Are Dokku cron tasks enough to replace Heroku Scheduler?
For many small, single-server workloads, yes, but only if the jobs can tolerate a single host and you add your own alerting. Dokku reads cron tasks from an app.json manifest based on Heroku's format, so recurring commands can be declared alongside the app. Dokku runs on a single server, so a host failure stops every scheduled job on it. Heroku itself says execution is expected but not guaranteed, with jobs in rare instances skipped or run twice. Your replacement should therefore make missed and duplicated runs visible, which Dokku's cron feature does not do for you by default. Check the Dokku application deployment docs and the app.json format before assuming parity for your job types.
What do Kubernetes CronJobs add over a single-server cron setup?
Kubernetes CronJobs add scheduling on a cluster that can place jobs on other nodes, keep run history and apply retry and concurrency settings. They also bring the cluster to operate: control plane nodes, etcd backups, upgrades on a release cycle of about three releases per year, and access control for who can change schedules. The Kubernetes project maintains the three most recent minor releases, so version upgrades are a recurring task. Choose CronJobs when you already run Kubernetes for other workloads or have a clear need for multi-node placement. For a handful of jobs on one app, the cluster overhead is usually larger than the scheduling benefit.
What does the team need to run scheduled jobs reliably?
The team needs four skills: Linux and container operations, a way to alert on failures, a defined owner for each job and a tested procedure for reruns. Each job should be idempotent or guarded by a lock, because a job that runs twice must not charge a customer twice or send duplicate emails. Logs must be kept somewhere that survives a container restart, and failed runs need an alert route to a person or on-call rota. Heroku's documentation recommends a custom clock process for critical jobs, which is a useful pattern to evaluate on your own platform. Confirm these responsibilities with your engineers before the cutover.
How long does replacing Heroku Scheduler take?
Duration depends on scope, not on a fixed rule: the number of jobs, their runtime, their data dependencies and how much monitoring you want. A small set of idempotent jobs on one app is a different project from scheduled work that writes to several databases and calls third-party APIs. Heroku Scheduler jobs are one-off dynos with a runtime limit tied to their interval, so long jobs often need redesign first. The fastest route is an inventory of every scheduled job with its interval, runtime, failure impact and owner. Ask any implementation partner for a timeline tied to that written inventory and a fixed scope.
What recurring costs come after the scheduler moves?
Recurring costs are the server or cluster capacity that runs the jobs, the monitoring and log storage that make failures visible, backups for the data the jobs touch, and the on-call time that handles failed runs. Heroku Scheduler itself is a free add-on, but its jobs count toward your dyno usage, so compare against your current dyno bill. On a self-managed host, the recurring line items are infrastructure, patching, certificate and dependency updates, and alert handling. Ask for each of these as a separate line in any quote, and for who pays for each one after handover.
Can our team replace Heroku Scheduler without outside help?
Many teams can, if they already operate Linux servers and have someone who owns the scheduled jobs. The work is mostly moving commands into app.json or a CronJob manifest, adding locks and logging, and writing alerts. Teams with no on-call rota, no monitoring stack or no experience with containers often find the monitoring and rerun procedures take the most effort. DeployOpen plans, implements, integrates, hardens and documents self-hosted platforms in your own infrastructure, and most engagements are fixed-scope with the scope agreed in writing first. Ongoing support is available as a retainer.
Which Heroku Scheduler behaviors should we test before cutover?
Test the behaviors that Heroku itself does not guarantee, because your replacement has to handle them on purpose:
- Missed runs, when a host is down or a scheduler fails to start the job
- Duplicate runs, when a job executes twice in one interval
- Overrun, when a job runs longer than its interval and must be stopped or skipped
- Failure alerts, confirming that a non-zero exit reaches a person
Run each case in staging with a deliberately broken job. Record what happens and who is notified, and keep those results as evidence for your change record.
Does moving scheduled jobs change our security or compliance posture?
Yes, because jobs often hold database credentials and API tokens, and where those credentials live changes your control set. On Dokku, environment variables and app configuration live on the host, so host access controls and backup encryption matter. On Kubernetes, Secrets are stored unencrypted in etcd by default, so you must configure encryption at rest, restrict list and watch access through RBAC, or use an external secrets store. Keep job logs free of secrets and record who can change a schedule. DeployOpen prepares controls and evidence for compliance assessments, but independent auditors issue any audit report or certification.
What the cost of self-hosted scheduled jobs includes
The main cost of self-hosted scheduled jobs is not the scheduler itself. Heroku Scheduler is a free add-on, but each run uses a one-off dyno that counts toward monthly usage, so the Heroku line item is really the compute for every job run. On a self-hosted platform that compute moves onto servers you pay for each month, whether Dokku runs cron tasks on one host or Kubernetes runs CronJobs on a cluster. Ask for the server count, instance sizes and whether the quote includes a second host for the jobs, since a single job host is a single point of failure for every scheduled task.
The larger one-time and recurring costs sit with people. Converting each job means reading the Heroku schedule, moving it into an app.json cron entry or a CronJob manifest, and testing that it runs the same work, including overlap and timeout behavior. Monitoring is also new work. Heroku's documentation says a scheduled dyno is terminated once it runs longer than its interval, and Heroku's Scheduler documentation expects that some runs may be skipped or run twice. Self-hosted jobs need logging, alerts on failed or missed runs, and a decision on whether a dead-man's-switch check or a job queue handles retries. Ask the vendor or partner to quote the conversion, the test plan and the alerting setup as separate line items.
Recurring operator time covers upgrades, patching of the host or cluster, and incident response when a job fails at 3 a.m. The Dokku and Kubernetes release cycles mean the platform needs regular upgrades, and the Kubernetes version skew policy sets the order in which control plane and node components must move. Confirm who owns each of these tasks, what on-call coverage is included, and whether a retainer covers upgrades and continuous monitoring. Compare these recurring items with the per-run compute you pay Heroku today, and keep the Heroku subscription in your model until the self-hosted schedule has run reliably through at least one full cycle of your business calendar.
How DeployOpen scopes and delivers scheduled job migrations
Every scheduled job migration starts with an inventory, not an implementation. For each Heroku Scheduler entry, we record the command, the frequency (every 10 minutes, hourly or daily), the dyno size, the database and external services it touches, the Heroku config vars it reads, and the business consequence if a run is skipped or happens twice. That inventory feeds a written scope that lists which jobs move to app.json cron tasks on Dokku, which move to Kubernetes CronJobs if you have chosen that path, which are retired, and which need a decision from your team first. Deliverables, acceptance checks and timelines are agreed in writing before any work begins, and most engagements are fixed-scope on that basis.
Implementation happens inside your own infrastructure, on servers or cloud accounts you control. Testing focuses on the behavior that Heroku's own documentation leaves to the operator: that each job runs once per schedule, that a run exceeding its interval is handled deliberately rather than silently killed, that idempotency holds when a run is repeated, and that failures raise an alert. Where a critical job needs stronger guarantees, the written scope can include a separate clock process or an external monitor for missed runs, and we test those paths before cutover. Documentation covers the job register, owners, runbooks for re-running a job by hand, and the evidence your auditors or internal reviewers may ask for.
DeployOpen does not publish prices. A quote follows from the written scope, so ask for line items that match the inventory: discovery, implementation, job testing, documentation and handover, plus an optional retainer for ongoing support, upgrades and monitoring after go-live. The retainer is recurring and the rest is typically one-time, and the size of the job inventory and the number of environments are the main drivers. Confirm with the vendor whether your Dokku or Kubernetes version and plugins are covered by the scope.

