Task scheduler

On this page 7

A task scheduler is the component of an automation system that starts jobs at defined times, intervals or events without a person triggering them. In scheduling for automation, it owns one decision only: when a unit of work should run, and what to do when that moment is missed, delayed or still occupied by a previous run.

Typical implementations range from the classic Unix cron daemon and the Windows Task Scheduler to systemd timers, Kubernetes CronJobs, the Quartz library and the schedulers inside workflow orchestrators. They share the same core model: a trigger that describes time, a job definition that describes the work, and a run history that records what actually happened.

How a task scheduler decides when to run a job

A task scheduler works by repeatedly comparing the current clock time with the next fire time of every registered trigger and dispatching each job whose time has come. After a run is dispatched, the scheduler computes the next fire time from the trigger definition and stores it, so the loop can continue indefinitely.

The dispatch step differs between designs. A local scheduler such as cron forks a process on the same machine. A distributed scheduler usually writes a message to a job queue, where a separate worker picks it up. This separation matters: the scheduler guarantees that work is requested on time, while the queue and the workers determine when it actually executes and whether it succeeds.

Most production schedulers also persist their state in a database. Persistent job stores let a scheduler survive a restart, detect runs that were missed while it was down and keep an auditable record of every attempt, which an in-memory timer cannot do.

Trigger types and schedule expressions

Schedulers express time through a small set of trigger types, and choosing the right type prevents most configuration errors. The following table compares the common types and what each is suited for.

Trigger typeHow time is definedTypical useMain pitfall
Calendar (cron) triggerFields for minute, hour, day and monthNightly reports, month-end closingTime zone and daylight saving shifts
Fixed-interval triggerEvery N seconds or minutesPolling, health checks, cache refreshDrift if measured from job end
One-time triggerA single absolute timestampScheduled publication, remindersLost if the store is not persistent
Event triggerSystem or application eventRun at startup, logon or file arrivalEvent storms causing bursts of runs
Dependency triggerCompletion of upstream jobsData pipelines in orchestratorsStalls when one upstream job hangs
Common trigger types in task scheduling

The standard cron expression has five fields: minute (0-59), hour (0-23), day of month (1-31), month (1-12) and day of week (0-6, with Sunday as 0 and often also 7). Quartz-style expressions add a leading seconds field and an optional year, so the same schedule needs six or seven fields there. Copying an expression between the two dialects without adjusting the fields is a frequent source of silent errors.

ExpressionMeaningRuns per day
*/15 * * * *Every 15 minutes96
0 * * * *At minute 0 of every hour24
30 2 * * *Every day at 02:301
0 6 * * 1-5At 06:00 Monday to Friday1 on weekdays, 0 on weekends
0 0 1 * *At midnight on the first day of each month1 on that day only
Example five-field cron expressions

One cron rule surprises many practitioners: when both day of month and day of week are restricted, classic cron runs the job when either field matches, not when both match. The expression 0 9 13 * 5 therefore fires on every 13th and on every Friday, not only on Friday the 13th.

Task scheduler, job queue and worker compared

A task scheduler, a job queue and a worker are three distinct roles that are often bundled in one product but should be reasoned about separately. The scheduler answers when, the queue answers where work waits, and the worker answers who executes it.

ComponentCore responsibilityHolds state aboutTypical failure
Task schedulerCreates runs at the right momentTriggers and next fire timesMissed or duplicated triggers
Job queueBuffers runs until capacity is freePending and in-flight messagesBacklog growth under load
WorkerExecutes the job logicThe current run onlyCrashes, timeouts, resource exhaustion
Delayed jobA queued item with a not-before timeOne future executionClock skew between producers
Division of responsibilities in a scheduled workload

Retries also belong to a different layer. A job retry re-executes a failed run, usually under the queue’s or worker’s policy, whereas the scheduler normally creates the next regular run on its own timetable regardless of whether the previous one succeeded.

Missed runs, overlaps and time zones

The behavior of a task scheduler at the edges, rather than on a normal day, determines whether a scheduled workload is reliable. Three edge cases need an explicit policy: missed runs, overlapping runs and clock changes.

A run is missed, or misfires, when the scheduler was down, paused or overloaded at the fire time. Policies range from skipping the run to executing it once as soon as possible or replaying every missed occurrence. Quartz, for example, treats a run as misfired only after a misfire threshold of 60 seconds by default, and systemd timers replay a missed run at boot only when Persistent=true is set.

An overlap occurs when a run is still active at the next fire time. Kubernetes CronJobs make the choice explicit through a concurrency policy with three values: Allow, Forbid and Replace. For jobs that write shared data, forbidding concurrent runs is the safe default.

Daylight saving time causes the subtlest errors. A job set for 02:30 local time may be skipped on the spring-forward day and may run twice on the fall-back day, depending on the implementation. Running schedules in UTC or setting an explicit time zone on each trigger removes the ambiguity.

SituationRecommended policyReason
Hourly metric collection was missedSkip and wait for the next runOld samples have little value
Daily billing export was missedRun once as soon as possibleThe output is required exactly once per day
Long report overlaps the next runForbid concurrent runsPrevents double writes and lock contention
Cache refresh overlaps the next runReplace the running jobOnly the newest result matters
Schedule tied to a business dayExplicit time zone on the triggerAvoids shifts at daylight saving changes
Edge-case policies and when to use them

Running a task scheduler reliably at scale

At scale, the main risk is that several scheduler instances fire the same trigger and the job runs more than once. Distributed schedulers prevent this with leader election or a database lock per trigger, so only one instance dispatches each occurrence. Even then, network failures can produce a duplicate dispatch, which is why scheduled jobs should be idempotent: running them twice must leave the same result as running them once.

  • Spread start times instead of placing every job at midnight; adding random jitter of a few seconds or minutes avoids load spikes on shared databases.
  • Set a timeout for every job, so a hung run does not block later runs under a forbid policy.
  • Monitor for absence, not only for failure: an alert that fires when a daily job has not succeeded within 26 hours catches a silently disabled trigger.
  • Run under a dedicated identity with only the permissions the job needs, rather than a personal user account.
  • Keep schedules in version control so changes to timing are reviewed like changes to code.

In simple terms

A task scheduler is an alarm clock for computer jobs. Someone tells it what should happen and when, for example sending a sales report every weekday at six in the morning, and it starts that job at the right moment without anyone pressing a button.

The hard part is not the normal day but the unusual one. The scheduler must know what to do if the computer was switched off at six, if yesterday’s report is still running, or if the clocks change for summer time. A good scheduler has a clear rule for each of these cases.

A practical example

An online retailer must send a reconciliation file to its payment provider once per day. The operations team defines a calendar trigger with the expression 15 2 * * * in UTC, so the export starts at 02:15 UTC every day and is unaffected by local daylight saving changes. The scheduler places the run on a job queue, and a worker generates the file, which normally takes about 40 minutes.

The team sets a 90-minute timeout, forbids concurrent runs and configures the misfire policy to run the export once as soon as possible after an outage. One night the scheduler cluster restarts at 02:10 and comes back at 02:25. Because the trigger state is persistent, the scheduler detects the missed occurrence and dispatches it immediately. The file arrives at 03:05, well before the provider’s 06:00 cut-off, and no day is skipped or sent twice.