Cron Expression Parser & Next-Run Preview: Code Examples

Translate any cron expression into plain English and see the next 5 run times.

ZERO UPLOAD · ALL LOCAL
  1. Type or paste a 5-field cron expression into the input (e.g. */15 * * * *).
  2. Use the preset buttons to load a common schedule instantly.
  3. Edit individual fields (MIN, HR, DOM, MON, DOW) — the expression updates live.
  4. Read the SCHEDULE panel for a plain-English description of the pattern.
  5. Choose a timezone and clock format, then read NEXT 5 RUNS for upcoming execution times.
PRESETS

MIN
HR
DOM
MON
DOW
SCHEDULE

NEXT 5 RUNS

    Linux Crontab Cron Syntax

    The Linux crontab uses a standard 5-field format: minute (0 to 59), hour (0 to 23), day-of-month (1 to 31), month (1 to 12), and day-of-week (0 to 7, where 0 and 7 are both Sunday). Each field accepts a value, a range (1-5), a list (1,3,5), a wildcard (*), or a step (*/15).1

    Syntax at a glance

    Linux crontab uses 5 space-separated fields: minute (0 to 59), hour (0 to 23), day-of-month (1 to 31), month (1 to 12), and day-of-week (0 to 7, where 0 and 7 are both Sunday). Each field accepts a literal value, a wildcard (*), a range (1-5), a comma-separated list (1,3,5), or a step (*/15). Multiple operators can appear in the same field: 1,10-20,30 is valid. Named day shortcuts (MON to SUN) work on most Linux cron implementations. Combining all five fields gives you fine-grained control over every minute, hour, day, and month. The order of the fields is fixed, so putting the minute value in the hour position silently produces a different schedule than the one you intended.

    Platform-specific differences

    Linux crontab evaluates schedules using the system clock timezone by default. Adding CRON_TZ=America/New_York above a job line overrides the timezone for that job and all subsequent lines, without changing the system clock.2 The SHELL variable defaults to /bin/sh; scripts using bash syntax need an explicit SHELL=/bin/bash header.2 The MAILTO variable controls where job output goes: a non-empty value mails it to that address, an empty string discards it.3 Root can read any user's crontab with crontab -u <username> -l.4 System-wide jobs live in /etc/crontab and /etc/cron.d/, and the /etc/crontab file uses an extra user field that personal crontabs do not, which is a common source of confusion when migrating entries between the two locations.5

    Common pitfalls

    Using relative paths in commands fails because cron provides a minimal PATH that may not match your interactive shell. A script that runs fine in a terminal silently fails in cron when the executable is outside that PATH. Bash-specific syntax ([[ ]], source, arrays) breaks when SHELL is /bin/sh, producing cryptic errors in the log. Crontab entries are text files consisting of lines, so ending the file with a newline avoids parser warnings on stricter implementations. The most destructive mistake is typing crontab -r instead of crontab -e: -r deletes the entire crontab without confirmation. Backing up your current crontab with crontab -l > crontab.bak before editing gives you a recovery path if a typo removes multiple entries at once.

    Setting PATH and SHELL for reliable cron execution

    Because Linux cron provides a minimal environment (SHELL=/bin/sh), any command that relies on an extended PATH or bash-specific syntax requires an explicit environment header in the crontab. Adding SHELL=/bin/bash and PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab file ensures that bash-syntax scripts find their interpreter and that executables in /usr/local/bin are reachable.1

    Simulating the cron environment before deploying

    To simulate cron's minimal environment before deploying a new job, run: env -i HOME=/home/youruser SHELL=/bin/sh PATH=/usr/bin:/bin /bin/sh -c 'your-command'. This reproduces cron's starting conditions without waiting for the scheduler to fire. If the command fails in this simulated environment, fix the missing path or interpreter before adding the entry to the crontab. The simulation catches environment problems that would otherwise require waiting for the next scheduled run to appear in the log.

    The simulation also exposes path mistakes before they reach production. Because cron runs with a minimal PATH, a command that resolves instantly in your interactive shell may not be found at all when the daemon spawns it. Repeating the run with env -i shows you the exact failure the scheduler would hit, so you can switch to absolute paths or extend PATH in the crontab rather than discovering the gap from a missed backup.

    Viewing cron activity in system logs

    On systemd-based distributions, journalctl -u cron or journalctl -u crond lists all daemon activity, including job start and completion records, and the answer depends on which init system your distribution uses.6 Filtering the journal output for today narrows the review to the current day without drowning in weeks of history. On older distributions using syslog, check /var/log/syslog or /var/log/cron for lines prefixed with the CRON keyword to find the relevant entries.6

    Checking whether cron attempted the job

    When a job does not appear in the system log at the expected time, the daemon did not attempt to run it: check the expression first, not the command. Cross-check by running crontab -l to confirm the entry is present, then paste the expression into a parser to verify it matches the expected time.4 If the parser shows the expected time but the logs stay empty, compare the machine timezone with the timezone assumption in your crontab. This separates schedule mistakes from command failures before you start editing the script.

    Reading the result after the command starts

    A logged start line does not prove that your command succeeded. After the daemon spawns the subprocess, you still need to verify three separate signals: the application-level output, the exit code, and the behavior of the configured MAILTO destination. On systemd-based distributions, journalctl -u cron records the subprocess spawn, but the job's own output reaches only the log file you configured with redirection or the mailbox of the MAILTO recipient. Reading the result closes the loop between scheduling and execution, so you catch silent problems before they become incidents.

    Notes

    Cron uses the system clock timezone. Run crontab -e to edit the current user's crontab. Logs go to /var/log/cron or /var/log/syslog depending on the distribution. The shell for cron jobs is /bin/sh; shell-specific bash syntax may fail without an explicit SHELL=/bin/bash header line in the crontab. Use absolute paths for all commands since PATH in cron is minimal.

    Examples

    Every 5 minutes

    */5 * * * * /usr/bin/mycommand

    Daily at 3 AM

    0 3 * * * /usr/bin/backup.sh

    Weekdays at 9 AM

    0 9 * * 1-5 /usr/bin/report.sh

    First of month at midnight

    0 0 1 * * /usr/bin/billing.sh

    @daily shortcut

    @daily /usr/bin/cleanup.sh

    @daily is equivalent to 0 0 * * *. Other shortcuts: @hourly, @weekly, @monthly, @yearly, @reboot.

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Every 5 minutes

    */5 * * * * /usr/bin/mycommand
    Sources
    1. 1.

      The Open Group, "crontab," pubs.opengroup.org, 2008. https://pubs.opengroup.org/onlinepubs/9699919799.orig/utilities/crontab.html

    2. 2.

      Michael Kerrisk, "crontab(5) - Linux manual page," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man5/crontab.5.html

    3. 3.

      Arch Linux, "crontab(5) - Arch manual pages," man.archlinux.org, accessed June 2026. https://man.archlinux.org/man/crontab.5

    4. 4.

      Michael Kerrisk, "crontab(1) - Linux manual page," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man1/crontab.1.html

    5. 5.

      Red Hat, "Chapter 24. Automating System Tasks," docs.redhat.com, accessed June 2026. https://docs.redhat.com/en/documentation/Red_Hat_Enterprise_Linux/7/html/system_administrators_guide/ch-automating_system_tasks

    6. 6.

      Red Hat, "Chapter 6. Troubleshooting problems by using log files," docs.redhat.com, accessed June 2026. https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/configuring_basic_system_settings/assembly_troubleshooting-problems-using-log-files_configuring-basic-system-settings

    FAQ

    GitHub Actions Schedule Cron Syntax

    GitHub Actions supports cron-based scheduling via the on.schedule trigger. Scheduled workflows default to UTC, but you can add an IANA timezone string for timezone-aware scheduling. The schedule uses POSIX cron syntax with five fields: minute, hour, day-of-month, month, and day-of-week.1

    Syntax at a glance

    GitHub Actions schedule syntax uses standard 5-field cron inside on.schedule.cron YAML keys. The expression is written as a YAML string, for example cron: '0 0 * * *'. Multiple entries under a single on.schedule block are valid; each fires the workflow independently. All five standard operators (*, -, ,, /) work for minute, hour, day-of-month, month, and day-of-week fields, and the same wildcard and step rules you use in a Linux crontab apply here as well.2 The shortest supported interval is once every 5 minutes, so expressions that would fire more frequently are silently rejected by the workflow parser.

    Platform-specific differences

    Scheduled workflows run on the latest commit on the default branch; a schedule defined on a feature branch has no effect until merged. In public repositories, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days, visible in the Actions tab where a banner appears. The on.workflow_dispatch trigger can be added alongside on.schedule to allow manual runs from the Actions tab, GitHub CLI, or REST API without waiting for the next scheduled time.3 A workflow can define multiple cron entries under a single on.schedule block, and each one fires independently, so you can schedule both a morning and an evening run without creating two separate workflow files.

    Common pitfalls

    The UTC default catches most teams off guard: 0 9 * * 1-5 fires at 9 AM UTC unless you add a timezone, which is 4 or 5 AM US Eastern time depending on DST. The 60-day inactivity disable silently stops workflows on quiet repositories; always re-enable from the Actions tab after periods of inactivity.

    Separating timing from branch behavior

    Forgetting that schedules only run on the default branch means testing a schedule on a feature branch produces no runs, which can mislead you into thinking the syntax is wrong when the branch is the actual issue. Before you merge, treat the schedule as deployment configuration rather than a disposable comment. Add a short note in the pull request description explaining the intended schedule so reviewers can verify the timing alongside the workflow logic.

    Treat the schedule as part of your release checklist rather than an afterthought. Because GitHub only evaluates schedules on the default branch, a change that works locally never reaches the scheduler until it merges, and a quiet repository can also lose its scheduled runs to the 60-day inactivity rule. Documenting the intended time in the pull request keeps that behavior visible to reviewers who may never open the workflow file itself.

    Converting your target time to UTC

    Because GitHub Actions defaults schedules to UTC, knowing your UTC offset is the first step for any workflow schedule. A morning CI run at 9 AM London time (UTC+0 in winter, UTC+1 in summer) requires 0 9 * * * in winter but 0 8 * * * in summer to fire at the same local clock time. If you need local-time scheduling, add the matching IANA timezone string instead of tracking DST manually, and add a comment above the cron entry documenting the corresponding local time.1

    Scheduling for globally distributed teams

    For teams spread across time zones, the safest approach is to schedule non-urgent jobs at UTC midnight or UTC noon, which translates to reasonable hours for the majority of the team regardless of the season. Reserve exact local-time scheduling for jobs that truly depend on business hours, and accept that DST spring-forward transitions can skip local times on timezone-aware platforms.

    Verifying your schedule before merging

    Before merging a scheduled workflow to your default branch, paste the cron expression into the cron parser above and read the plain-English description. Confirm the day-of-week field matches your intent: 1-5 covers Monday through Friday; 1 alone covers Monday only.3 Check the minimum interval rule by counting how often the expression fires per hour.

    Checking schedule acceptance after merge

    After merging, navigate to Actions and switch the filter to "schedule" triggers to monitor the first few runs. GitHub logs the exact UTC dispatch time, not the target local time from the expression. A run listed at 13:01 UTC for a 0 13 * * * expression is normal; high load at the top of the hour can delay scheduled workflows. Keep the first run under observation because it proves the workflow file is on the default branch, the cron expression is accepted, and the repository is not in the 60-day disabled state. If the schedule is correct but the job fails, you have separated timing from execution and can debug the workflow steps directly.

    Notes

    Scheduled workflows run on the latest commit on the default branch. GitHub enforces a minimum interval of every 5 minutes. During periods of high load GitHub may delay scheduled runs. Public repositories with no activity for 60 days have scheduled workflows disabled until re-enabled from the Actions tab.

    Examples

    Daily at midnight UTC

    on:
      schedule:
        - cron: '0 0 * * *'

    Every 15 minutes

    on:
      schedule:
        - cron: '*/15 * * * *'

    Monday at 9 AM UTC

    on:
      schedule:
        - cron: '0 9 * * 1'

    Multiple schedules

    on:
      schedule:
        - cron: '0 6 * * *'
        - cron: '0 18 * * *'

    Multiple entries run the same workflow at different times.

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Daily at midnight UTC

    on:
      schedule:
        - cron: '0 0 * * *'
    Sources
    1. 1.

      GitHub, "Events that trigger workflows," docs.github.com, accessed June 2026. https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows

    2. 2.

      Michael Kerrisk, "crontab(5) - Linux manual page," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man5/crontab.5.html

    3. 3.

      GitHub, "Manually running a workflow," docs.github.com, accessed June 2026. https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow

    FAQ

    AWS EventBridge Cron Expression Format

    When EventBridge has to run a rule at a calendar time, you write a modified 6-field cron expression: minute, hour, day-of-month, month, day-of-week, and year. Unlike standard cron, you must specify either day-of-month or day-of-week; the other must be a question mark (?).1

    Syntax at a glance

    AWS EventBridge cron expressions follow a 6-field format: minute hour day-of-month month day-of-week year. The expression wraps in cron(): cron(0 18 * * ? *). Either day-of-month or day-of-week must be a question mark; setting both to non-wildcards is invalid. Named shortcuts (SUN to SAT, JAN to DEC) work. Two extra operators are available: L (last, meaning the last day of the month, or 6L for the last Saturday) and # (ordinal, where 2#1 selects the first Monday). The alternative rate() syntax (rate(5 minutes), rate(1 day)) is simpler for fixed intervals. The year field is required and is frequently set to a range such as 2024-2030 to bound the schedule's validity window.1

    Platform-specific differences

    All EventBridge cron() and rate() schedules run in UTC, and the minimum interval is 1 minute for both cron() and rate(), so translating a local business-hour schedule into UTC equivalents is a necessary first step before writing the expression.2 L and # operators are supported in EventBridge but not in standard Unix cron, making expressions that use them non-portable to other schedulers.3

    Choosing between cron() and rate()

    Use cron() when the rule must land on a calendar boundary, such as midnight UTC, the first Monday of a month, or a weekday business window. Use rate() when the interval matters more than the clock time, such as polling every 15 minutes or refreshing a cache once per hour. This choice keeps the manifest readable and prevents you from forcing a fixed interval into a calendar expression.

    Keep in mind that rate() resets its clock every time you edit the rule, so rate(1 day) tracks the last update rather than a fixed wall-clock time. When the job must align with a reporting deadline or a midnight UTC boundary, cron() is the safer choice because it pins the run to an absolute moment. Reserve rate() for background work where the precise hour does not matter to anyone downstream.

    Common pitfalls

    Forgetting the ? placeholder is the most common EventBridge mistake: cron(0 9 * * MON-FRI *) is invalid; the day-of-month field must be ?, giving cron(0 9 ? * MON-FRI *). The year field catches developers accustomed to 5-field cron: a missing sixth field causes a parse error. Standard Unix cron uses five fields with Sunday as 0 in POSIX, while EventBridge uses Sunday as 1. Copying a Unix cron expression directly into EventBridge without adjusting day-of-week numbering produces a schedule that fires on the wrong day.34

    Checking the next trigger times

    Before you deploy, translate the expression into plain language and compare the next several trigger times with the business requirement. A schedule that looks correct as text can still be wrong for the intended timezone, weekday, or month boundary. This final review is especially important when a rule controls billing, cleanup, or reporting jobs that affect downstream teams. A monthly rule that fires on the 31st will skip seven months of the year unless you account for that gap in the design.

    Testing cron() expressions before deployment

    Testing cron() expressions in EventBridge before deployment catches the most expensive mistakes: day-of-week numbering errors and missing ? placeholders. The AWS console EventBridge rule editor validates the expression and shows the next 10 trigger times when you save a rule.2 Reviewing those trigger times against your intended schedule costs seconds and prevents a misconfigured rule from firing all weekend instead of on weekdays.

    Using a standard 5-field parser as a cross-check

    For programmatic pre-validation, convert the cron() expression to standard 5-field format by stripping the year field and the ? placeholder while shifting each day-of-week number down by one, since EventBridge uses Sunday equals 1 where standard cron uses Sunday equals 0. Running the converted expression through a browser-based parser catches field-order errors and range violations before you touch the AWS console, which saves you from deploying a broken rule that could affect production workloads.

    Rate expressions as the simpler alternative

    When your schedule is a fixed interval rather than a calendar pattern, rate() expressions remove the ? placeholder complexity entirely. rate(1 hour) fires every hour; rate(1 day) fires every 24 hours from the moment the rule was created. The interval resets each time you modify the rule, so rate(1 day) does not guarantee a midnight UTC run.

    For schedules that must fire at a specific UTC time or on specific days, cron() is the only option. For health checks, polling jobs, and data refresh cycles where the exact clock time matters less than the interval, rate() is simpler to read, less error-prone to write, and portable to EventBridge Scheduler without modification.

    Notes

    All EventBridge schedules run in UTC. The expression is wrapped in cron(): cron(minute hour dom month dow year). As an alternative, rate() expressions (rate(5 minutes), rate(1 hour), rate(1 day)) are simpler for fixed intervals and avoid the 6-field format entirely. Rates under 1 minute are not supported. AWS recommends EventBridge Scheduler for new scheduled tasks.

    Examples

    Daily at 6 PM UTC

    cron(0 18 * * ? *)

    Every 5 minutes via rate()

    rate(5 minutes)

    Weekdays at 9 AM UTC

    cron(0 9 ? * MON-FRI *)

    First Monday of the month

    cron(0 10 ? * 2#1 *)

    #1 means the first occurrence of that weekday in the month.

    Try in the tool

    What this page covers

    • 6-field format minute hour day-of-month month day-of-week year, wrapped in cron(...)
    • Required ? placeholder day-of-month or day-of-week must be ?; both cannot be set to concrete values
    • Day-of-week numbering EventBridge uses Sunday = 1, unlike standard Unix cron's Sunday = 0
    • L and # operators supported in EventBridge (6L = last Saturday, 2#1 = first Monday) but not in standard 5-field cron

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Try it in the tool ↑
    Sources
    1. 1.

      Amazon, "Setting a schedule pattern for scheduled rules (legacy)," docs.aws.amazon.com, accessed June 2026. https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html

    2. 2.

      Amazon, "Creating a scheduled rule (legacy)," docs.aws.amazon.com, accessed June 2026. https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html

    3. 3.

      Michael Kerrisk, "crontab(5) - Linux manual page," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man5/crontab.5.html

    4. 4.

      The Open Group, "crontab," pubs.opengroup.org, 2008. https://pubs.opengroup.org/onlinepubs/9699919799.orig/utilities/crontab.html

    FAQ

    Kubernetes CronJob Schedule Field Reference

    A Kubernetes CronJob uses standard 5-field cron in its spec.schedule field. By default it runs in the timezone of the kube-controller-manager, and Kubernetes 1.27 added stable support for the spec.timeZone field.1

    Syntax at a glance

    A Kubernetes CronJob uses standard 5-field cron syntax in the spec.schedule field, expressed as a quoted string: schedule: "0 2 * * *". Kubernetes documents the minute, hour, day-of-month, month, and day-of-week fields, and it accepts the standard wildcard, range, list, and step operators.1 Named day shortcuts are documented as sun, mon, tue, wed, thu, fri, sat, and the same page also documents @-shortcut macros such as @daily and @hourly. If you need an expression that is portable outside Kubernetes, the explicit 5-field form is still the safest choice because the same syntax works in a Linux crontab, a CI pipeline scheduler, or a cloud event rule without modification.

    Platform-specific differences

    The CronJob API defines spec.concurrencyPolicy as Allow (default), Forbid, or Replace, and it defines spec.timeZone as the time zone name used for the schedule when the kube-controller-manager timezone should not apply.2 The same API reference defines spec.startingDeadlineSeconds as the deadline, in whole seconds, for starting a job that missed its scheduled time. Choosing the right concurrency policy requires understanding what happens when a job runs longer than the interval between scheduled runs, because the default Allow behavior can let two instances run concurrently and corrupt shared state.

    Preventing overlap and missed runs

    For a job that writes to a database, calls an external API, or updates shared files, Forbid is often the safest policy because the controller will not start a second Job while the previous one is active. Replace is more aggressive: it deletes the current Job before starting the next one, which can be useful for stateless refresh tasks but dangerous for operations that must finish cleanly. StartingDeadlineSeconds is not a backfill promise; it is a grace period. If the controller cannot start the missed occurrence within that window, Kubernetes skips it and waits for the next schedule.

    The timezone also matters for overlap behavior because the schedule is evaluated against the kube-controller-manager clock unless you set spec.timeZone. On older clusters that predate the 1.27 field, the only way to shift the run time is to change the controller configuration or accept UTC, which surprises teams that assume their region is in effect. Pairing the right concurrency policy with a sensible startingDeadlineSeconds value keeps a brief outage from either flooding the cluster or silently dropping the job.

    Common pitfalls

    Omitting spec.concurrencyPolicy leaves the default Allow behavior in place, so overlapping runs are possible for long-running jobs. The spec.timeZone field requires Kubernetes 1.27 or later; older clusters may reject manifests that use fields they do not know. A spec.startingDeadlineSeconds value shorter than the delay before the controller can start a missed job causes that missed occurrence to be skipped, while future scheduled occurrences continue.2

    Aligning policy with job side effects

    Before you apply a manifest, ask what should happen when the cluster is down or a job runs longer than expected. The answer determines the policy fields, not the other way around. A job that updates a shared database benefits from Forbid so a second run never starts before the first finishes, while a stateless refresh task can safely use Replace to ensure the newest run always takes precedence. This keeps the CronJob aligned with the operation it performs rather than with the shape of the original crontab line.

    Monitoring a CronJob after deployment

    For monitoring a CronJob after deployment, label the Jobs created by the CronJob and use those labels to select related Pod logs. Filtering kubectl logs by the generated Job label is useful for checking a recent run, and the kubectl logs implementation supports selector-based log retrieval plus recent time windows so you can narrow the output to a specific time range.3

    Selecting logs from related Jobs and Pods

    Production CronJobs should surface failures outside the dashboard as well. A common pattern is to add stable labels to the CronJob job template, then use those labels when querying logs or wiring alerts to the corresponding Job and Pod resources. The labels also make it easier to inspect whether a missed schedule, a failed Pod, or a command error caused the problem.

    Migrating from crontab to a CronJob

    Moving a Linux crontab job to a Kubernetes CronJob requires containerising the command, writing a CronJob manifest, and verifying the schedule converts correctly. The 5-field cron syntax carries over directly; only the timezone handling changes. In Kubernetes 1.27+, use the spec.timeZone field for named IANA timezone scheduling. To trigger a one-off run from an existing CronJob, use kubectl create job with the CronJob source option. The kubectl create job implementation accepts a CronJob source and rejects non-CronJob resource types for that flag.4 Once the container runs correctly in isolation, write the CronJob manifest and confirm the first scheduled run completes before removing the legacy crontab entry.

    Notes

    Add a timezone with spec.timeZone: "America/New_York" (requires Kubernetes 1.27+, stable in 1.27). Set spec.concurrencyPolicy to Forbid to prevent overlapping runs, or Replace to kill the current job before starting the new one. The default Allow lets multiple instances run concurrently. spec.startingDeadlineSeconds prevents the controller from starting a job if it missed its window by more than N seconds.

    Examples

    Basic CronJob manifest

    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: my-job
    spec:
      schedule: "0 2 * * *"
      jobTemplate:
        spec:
          template:
            spec:
              containers:
              - name: job
                image: busybox
                command: ["/bin/sh", "-c", "date"]
              restartPolicy: OnFailure

    With timezone and Forbid policy (1.27+)

    spec:
      schedule: "0 9 * * 1-5"
      timeZone: "Europe/London"
      concurrencyPolicy: Forbid

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Basic CronJob manifest

    apiVersion: batch/v1
    kind: CronJob
    metadata:
      name: my-job
    spec:
      schedule: "0 2 * * *"
      jobTemplate:
        spec:
          template:
            spec:
              containers:
              - name: job
                image: busybox
                command: ["/bin/sh", "-c", "date"]
              restartPolicy: OnFailure
    Sources
    1. 1.

      Kubernetes, "CronJob," kubernetes.io, accessed June 2026. https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/

    2. 2.

      Kubernetes, "CronJob v1 API reference," kubernetes.io, accessed June 2026. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/cron-job-v1/

    3. 3.

      Kubernetes Authors, "kubectl logs source," github.com, accessed June 2026. https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/logs/logs.go

    4. 4.

      Kubernetes Authors, "kubectl create job source," github.com, accessed June 2026. https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/create/create_job.go

    FAQ

    systemd OnCalendar Timer Syntax

    On many Linux systems, systemd timers are a built-in alternative to cron for recurring commands.1 The OnCalendar= directive in a .timer unit accepts a calendar expression with a date-time shape similar to cron, but with explicit support for named weekdays, ranges, steps, omitted fields, and named time zones.

    Calendar expressions use the structured form DayOfWeek Year-Month-Day Hour:Minute:Second, where any component can be omitted or replaced with a wildcard. A common daily schedule such as *-*-* 09:00:00 leaves the weekday and year open, fixes the month and day, and runs at 9 AM.

    Syntax at a glance

    OnCalendar expressions follow the pattern [DOW] [Year-Month-Day] [Hour:Minute:Second]. Components can use * for any value, .. for ranges such as Mon..Fri, and / for steps such as */15 or Mon..Fri 00/2. Shorthand aliases include daily (*-*-* 00:00:00), hourly (*-*-* *:00:00), weekly (Mon *-*-* 00:00:00), and monthly (*-*-01 00:00:00). If the seconds component is omitted, systemd assumes :00. The weekday component is optional, so an expression like *-*-* 09:00:00 runs every day at 9 AM without specifying a day of the week, which is the most common shape for daily maintenance jobs. You can also combine multiple expressions in a single timer by separating them with a newline, which lets you schedule both a weekday and a weekend pattern in the same unit without creating a second file.2

    Platform-specific differences

    systemd calendar timers are wall-clock timers, and OnClockChange= plus OnTimezoneChange= can trigger the paired service when the realtime clock jumps or the local timezone changes; both options default to false. Persistent=true stores the last trigger time and can catch up a missed OnCalendar= timer when the timer is activated again, while WakeSystem=true can resume the system from suspend when the hardware and system support it, though WakeSystem requires both the firmware and the kernel to support wake-on-timer events.3 A named timezone can be included in the expression, for example OnCalendar=Mon *-*-* 08:00:00 Europe/Berlin, which lets the timer follow the wall clock in that zone without requiring the system timezone to match.2

    Common pitfalls

    If Persistent=true is omitted, a missed OnCalendar= timer does not necessarily catch up after the machine was off or asleep.4 Setting AccuracySec=1s narrows the firing window to one second, but it also increases wake frequency and prevents systemd from coalescing the timer with nearby wake-ups. Writing the unit files is not enough: enable and start the timer with systemctl enable and start my.timer, or enable it separately before rebooting.1

    Avoiding silent misses

    Before relying on a timer, decide whether missed runs should be backfilled. Persistent=true is useful for maintenance jobs that should eventually run after downtime, but it can be risky for jobs that should not execute late, such as a report that assumes the previous day is complete. This decision belongs in the timer design, not after the first missed run.

    A separate but related lever is AccuracySec, which controls how precisely the timer fires rather than whether it catches up. Leaving the default of one minute lets systemd batch this timer with other wake-ups, which is usually fine for a maintenance task but surprising if you expected the exact second. Decide both behaviors together, because a timer that backfills a late run but fires imprecisely can still produce confusing logs after downtime.

    Migrating from cron to systemd timers

    Replacing a crontab entry with a systemd timer usually requires two unit files: a .service file containing the command and a .timer file containing the OnCalendar= expression. Name them identically, such as mybackup.service and mybackup.timer, so the timer activates the matching service by default.3 The .service unit normally contains the command in [Service] as ExecStart=, and Type=oneshot is often appropriate for a command that runs and exits; RemainAfterExit=no is the default and keeps the service from staying active after the process exits.5

    Confirming the timer after installation

    Once the units are in place, enable and start the timer with systemctl enable and start mybackup.timer, then list timers with systemctl list-timers to confirm the next run. If the .service writes to standard output or standard error, set StandardOutput=journal or leave the default and inspect timer-triggered output with journalctl -u mybackup.service.5 Remove the old crontab line only after confirming the timer fires reliably.

    Choosing between AccuracySec values

    AccuracySec controls the trade-off between timing precision and system wake-up frequency. The default value of 1 minute means systemd may fire the timer up to 60 seconds after the target time, which lets systemd coalesce wake-ups with other timers firing in the same window.4 For most maintenance jobs, this is acceptable; the exact second of execution rarely matters for a nightly backup.

    When to use AccuracySec=1s

    For tasks that interact with time-sensitive external systems, such as a polling job that must run within seconds of a specific minute boundary, set AccuracySec=1s. This overrides the batching behavior and forces a near-exact trigger. Monitor CPU wake frequency if you set AccuracySec=1s on a battery-powered system or a server managed for power efficiency, since the tighter timing increases wake-up frequency across the system.

    Notes

    Run systemctl list-timers with all timers shown to see active and inactive timers and their next trigger times. After editing a .timer unit, run systemctl daemon-reload and restart the timer. Use journalctl for the paired service to inspect timer-triggered output.

    Examples

    Daily at 6 AM

    [Timer]
    OnCalendar=*-*-* 06:00:00

    Weekdays at 9 AM

    [Timer]
    OnCalendar=Mon..Fri *-*-* 09:00:00

    Every 15 minutes

    [Timer]
    OnCalendar=*:0/15

    First of month at midnight

    [Timer]
    OnCalendar=*-*-01 00:00:00

    Try in the tool

    What this page covers

    • Calendar expression shape [DayOfWeek] [Year-Month-Day] [Hour:Minute:Second], distinct from 5-field cron syntax
    • Shorthand aliases daily, hourly, weekly, monthly expand to their full calendar-expression equivalents
    • Persistent=true backfills a missed run after downtime; not the default behavior
    • AccuracySec defaults to 1 minute of firing slack; set to 1s to force near-exact timing at the cost of wake-up batching

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Try it in the tool ↑
    Sources
    1. 1.

      Oracle, "Using Timer Units to Control Service Unit Runtime," docs.oracle.com, accessed June 2026. https://docs.oracle.com/en/operating-systems/oracle-linux/10/systemd/UsingTimerUnits.html

    2. 2.

      Michael Kerrisk, "systemd.time(7)," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man7/systemd.time.7.html

    3. 3.

      systemd Project, "systemd.timer(5)," freedesktop.org, accessed June 2026. https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html

    4. 4.

      Michael Kerrisk, "systemd.timer(5)," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man5/systemd.timer.5.html

    5. 5.

      Oracle, "Changing systemd Service Unit Files," docs.oracle.com, accessed June 2026. https://docs.oracle.com/en/operating-systems/oracle-linux/10/systemd/ModifyingsystemdConfigurationFiles.html

    FAQ

    GitLab CI Pipeline Schedule Syntax

    GitLab CI supports scheduled pipelines via the Pipeline Schedules UI under CI/CD > Schedules. Schedules use standard 5-field cron syntax (minute hour day-of-month month day-of-week) and support a configurable timezone, unlike GitHub Actions which forces UTC. Schedules run on the default branch unless a different branch is specified, and they can pass custom CI/CD variables visible to the triggered pipeline.1

    Syntax at a glance

    GitLab accepts standard 5-field cron: minute hour day-of-month month day-of-week.2 Wildcards (*), ranges (1-5), lists (1,3,5), and steps (*/15) are all valid.3 Unlike AWS EventBridge, there is no 6th year field and no ? placeholder, so the syntax is closer to what you already know from Linux crontab or CI schedulers in other platforms. The timezone is set per-schedule in the UI, defaulting to UTC, which means you can write the expression in your local timezone and let GitLab convert it to the runner's UTC clock. Every scheduled pipeline runs against the latest commit on the configured branch, so a stale branch can make a working expression appear broken when the pipeline fails on outdated code.

    Platform-specific differences

    Each schedule can define custom variables that override or extend CI/CD variables for that run. A schedule can be enabled or disabled without deleting it. GitLab evaluates schedules periodically on shared runners; the actual trigger may be delayed by a few minutes. The CI_PIPELINE_SOURCE variable equals "schedule" inside a scheduled run, which lets you skip steps in regular push pipelines.4 Because GitLab evaluates schedules roughly every hour rather than at the exact minute, an expression like 0 * * * * may fire several minutes past the hour mark on a busy shared runner.1

    Accounting for GitLab schedule evaluation

    Treat GitLab schedules as best-effort calendar triggers, not precision timers. The expression decides which branches and times are eligible, while GitLab decides when to poll and start the pipeline. This distinction matters for jobs that depend on business deadlines, external API windows, or customer reporting cutoffs. If the work must start at an exact minute, keep the GitLab schedule as the trigger but let an external orchestrator or runner policy enforce the stricter timing. Keep the schedule description near the variable set so the next maintainer knows why the job runs at that cadence.

    Also remember that disabling a schedule is not the same as deleting it, and the entry keeps its branch and variable configuration intact while it is off. Because schedules fire against the latest commit on their configured branch, a schedule left pointing at an old branch can silently run stale code while you assume the fix was already deployed. Pair the cadence note with a branch and variable check whenever you revisit the schedule.

    Common pitfalls

    Setting the timezone in the UI but forgetting that the runner itself runs in UTC causes off-by-one-hour confusion in logs. Disabling a schedule in the UI (the toggle) differs from pausing a runner: the schedule entry still exists but will not trigger. Schedules do not inherit protected branch restrictions automatically; check your branch protection settings for sensitive jobs.

    Before you save a schedule, compare the expression with the intended branch, timezone, and variable set. A correct cron line can still produce the wrong pipeline if it targets an old branch or relies on variables that only exist in another environment. This final checklist separates expression mistakes from project configuration mistakes.

    Reviewing the schedule description

    Keep a short note near the schedule when the timing supports a business cutoff. A schedule description like 'runs hourly to stay within the third-party API quota' tells the next maintainer why the expression looks the way it does, and it prevents well-meaning edits that would push the job past its rate limit. Without that context, a reviewer might simplify the expression and accidentally break the external integration.

    Using CI_PIPELINE_SOURCE to separate scheduled from push pipelines

    When a GitLab pipeline can be triggered both by a commit push and by a scheduled run, the CI_PIPELINE_SOURCE variable lets you apply different rules to each trigger. Add rules: - if: '$CI_PIPELINE_SOURCE == "schedule"' to restrict a job to scheduled runs only. The negated form, if: '$CI_PIPELINE_SOURCE != "schedule"', skips a job during scheduled runs while including it on push triggers.4

    This pattern suits jobs that consume external API quotas or run long migration checks that are too slow for routine push pipelines. Schedule the heavy jobs at off-peak hours and exclude them from the push pipeline entirely, so your team gets fast feedback on regular commits while the scheduled pipeline handles comprehensive checks.

    Passing variables to a scheduled pipeline

    In the schedule configuration, each GitLab schedule can define its own set of CI/CD variables that override or extend project-level variables for that specific run. Open a schedule under CI/CD > Schedules and add key-value pairs under Variables. Those variables appear inside every job of the triggered pipeline and are accessible with the same $VARIABLE_NAME syntax as any other CI variable.

    Parameterizing one pipeline for multiple environments

    A common use of schedule variables is running the same .gitlab-ci.yml against different targets: one schedule sets ENVIRONMENT=production and fires weekly, another sets ENVIRONMENT=staging and fires nightly. Both schedules share a single pipeline definition but produce different behavior based on the variable value. This avoids duplicating pipeline files while giving you separate scheduling control per environment. When you parameterize one pipeline this way, add a comment above each schedule that names the target environment, so the next maintainer can distinguish the production run from the staging run without tracing variable values through the pipeline logic.

    Notes

    Use $CI_PIPELINE_SOURCE == "schedule" in rules: or only: to limit steps to scheduled runs. GitLab checks schedules roughly every hour; a 0 * * * * expression triggers once per hour but may fire a few minutes past the hour mark.

    Examples

    Daily at midnight UTC

    0 0 * * *

    Every 6 hours

    0 */6 * * *

    Weekdays at 9 AM (set timezone in UI)

    0 9 * * 1-5

    First day of month at 2 AM

    0 2 1 * *

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Daily at midnight UTC

    0 0 * * *
    Sources
    1. 1.

      GitLab, "Scheduled pipelines," docs.gitlab.com, accessed June 2026. https://docs.gitlab.com/ci/pipelines/schedules/

    2. 2.

      The Open Group, "crontab," pubs.opengroup.org, 2008. https://pubs.opengroup.org/onlinepubs/9699919799.orig/utilities/crontab.html

    3. 3.

      Michael Kerrisk, "crontab(5) - Linux manual page," man7.org, accessed June 2026. https://www.man7.org/linux/man-pages/man5/crontab.5.html

    4. 4.

      GitLab, "Specify when jobs run with rules," docs.gitlab.com, accessed June 2026. https://docs.gitlab.com/ci/jobs/job_rules/

    FAQ

    Vercel Cron Jobs

    In Vercel, scheduled work starts as a cron expression in vercel.json and arrives at your function as an authenticated HTTP GET request. The crons array maps each expression to a function path, while standard 5-field cron syntax defines the timing: minute, hour, day-of-month, month, and day-of-week. All schedules run in UTC.1

    Vercel's free Hobby plan allows 100 cron jobs with a minimum interval of once per day; the Pro plan supports up to 100 jobs and allows minute-level scheduling2. Treat the expression as routing configuration, not just a comment.

    Syntax at a glance

    Define crons in vercel.json under the "crons" key, each entry with a "path" (your API route) and a "schedule" (cron expression). Example: {"path": "/api/daily-sync", "schedule": "0 0 * * *"}. The minimum schedule on the Hobby tier is once per day; on Pro and Enterprise, once per minute. All times are UTC, so converting a local business-hour schedule to UTC equivalents is a necessary first step before writing the expression. The path must point to an existing API route in your project, and Vercel validates both the path and the expression at deploy time. If you rename a function file without updating the crons array, the deploy succeeds but the cron job starts returning 404 errors on every scheduled invocation.

    Platform-specific differences

    Vercel wraps each cron trigger in an authenticated HTTP GET request: the function receives a standard Request object and must return a Response. The Authorization header contains a bearer token that matches CRON_SECRET in your environment variables; validate it to prevent unauthorized calls3. Cron jobs count against your serverless function invocation limits and time out within the function's maxDuration.

    Treating the cron endpoint like an API route

    The schedule does not call your code directly. It calls a URL, so the same concerns that apply to public routes still apply: authentication, response status, timeouts, and observability. A cron endpoint should return a concise success body, avoid interactive prompts, and fail loudly when a required environment variable is missing. This keeps Vercel's dashboard useful because the HTTP status becomes the first clue when a run breaks.

    Because every trigger arrives as a plain HTTP request in UTC, the same rate and timeout rules that govern your other routes apply here as well. A cron function that runs long enough to exceed maxDuration fails the same way a slow API route would, and the failure shows up as a non-2xx status in the dashboard. Treat the scheduled path as production traffic from the moment it is deployed, because Vercel will call it on schedule whether or not you have load-tested it.

    Common pitfalls

    Missing CRON_SECRET validation lets anyone trigger your cron endpoint by guessing the path, which is especially dangerous when the endpoint performs write operations or consumes a limited external quota. Returning a non-2xx status code from the function marks the run as failed in the Vercel dashboard and can trigger retry behavior. Using Hobby-tier schedule intervals of less than 24 hours causes a validation error on deploy2.

    Before you deploy, test the route without the bearer header and confirm the response is 401. Then test it with the header and confirm the response is 2xx. This two-step check proves both sides of the contract: unauthorized callers are blocked and Vercel can complete the scheduled request. Skipping this check is the most common reason a cron job that works in testing starts failing silently after a deployment changes the function code.

    Reviewing run status after deployment

    Keep the verified route path beside the vercel.json entry so future edits do not break the mapping between the cron expression and the function that handles it. After deployment, open the Vercel dashboard, navigate to the Cron Jobs table, and confirm the job shows a green success status for its most recent run. If the status reads failed, inspect the function logs under the matching path to determine whether the handler rejected the request, timed out, or threw an unhandled exception.

    Handling CRON_SECRET validation in your handler

    For every Vercel Cron endpoint, validating the CRON_SECRET is the single most important security step. A function exposed at /api/nightly-sync that skips this check is publicly callable by anyone who discovers the URL. Add the validation as the first operation in your handler and return a 401 Response immediately if the Authorization header does not match.

    Validation pattern for TypeScript handlers

    For TypeScript handlers using the Vercel Edge Runtime, the check looks like: if (request.headers.get('Authorization') !== 'Bearer ' + process.env.CRON_SECRET) return new Response('Unauthorized', { status: 401 }). Test the validation by calling the endpoint without the header and confirming a 401 response before deploying. The Vercel dashboard logs the HTTP status code returned by each cron run, making it easy to confirm the handler rejected an unauthorized call.

    Logging cron run results in Vercel

    Vercel captures the HTTP response from each cron handler invocation and displays the status in the Cron Jobs dashboard, so a quick glance tells you whether the most recent run succeeded or failed without opening the function logs. A handler that returns a 200 response with a JSON body (such as { processed: 42, errors: 0 }) makes each run auditable without requiring an external logging service.

    For debugging a run that returned an unexpected status, navigate to Functions under your project and filter logs by the cron endpoint's path and the timestamp of the failed run. Vercel retains function logs for a plan-dependent duration; on the Hobby plan, logs are available for a few days. For longer log retention or structured querying, forward logs to an external observability platform using Vercel's log drain integration.

    Notes

    Check the "Cron Jobs" tab in the Vercel dashboard for the last run status and next scheduled time. Set CRON_SECRET in project environment variables and validate it in your handler: request.headers.get('Authorization') !== Bearer ${process.env.CRON_SECRET}`. Add export const maxDuration = 60;` to extend the function timeout beyond the 300-second default.

    Examples

    vercel.json entry (daily at midnight)

    {
      "crons": [
        {
          "path": "/api/nightly-sync",
          "schedule": "0 0 * * *"
        }
      ]
    }

    Weekly on Monday at 6 AM

    {"path": "/api/weekly-report", "schedule": "0 6 * * 1"}

    Every 5 minutes (Pro only)

    {"path": "/api/health-check", "schedule": "*/5 * * * *"}

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    vercel.json entry (daily at midnight)

    {
      "crons": [
        {
          "path": "/api/nightly-sync",
          "schedule": "0 0 * * *"
        }
      ]
    }
    Sources
    1. 1.

      Vercel, "Cron Jobs," vercel.com, accessed June 2026. https://vercel.com/docs/cron-jobs

    2. 2.

      Vercel, "Usage & Pricing for Cron Jobs," vercel.com, accessed June 2026. https://vercel.com/docs/cron-jobs/usage-and-pricing

    3. 3.

      codingcatdev, "How to Secure Vercel Cron Job routes in Next.js 14 (app router)," dev.to, accessed June 2026. https://dev.to/codingcatdev/how-to-secure-vercel-cron-job-routes-in-nextjs-14-app-router-33mp

    FAQ

    Render Cron Jobs

    For command-oriented jobs that already belong in a container, Render Cron Jobs offer a simple dashboard-driven scheduler. Each job specifies a Docker image or build command, a start command, and a standard 5-field cron schedule. All Render cron schedules use UTC, and cron jobs start at $1/month with a maximum run duration of 12 hours.1

    Syntax at a glance

    Render uses standard 5-field cron (minute hour day-of-month month day-of-week). Set the expression in the dashboard under the Cron Job's "Schedule" field. Cron jobs start at $1/month with billing prorated by the second of active running time.2 Ranges, lists, steps, and wildcards all work as in standard Unix cron.3 Render also lets you select a timezone per job, so the expression is evaluated in that zone with automatic DST handling, which means you can write the expression in your local wall-clock time without manually converting to UTC. The free tier limits the number of cron minutes per month, so monitoring your usage in thedashboard under Billing prevents surprise overage charges that could pause your scheduled jobs mid-cycle.

    Platform-specific differences

    Unlike Vercel (HTTP-triggered) or GitHub Actions (workflow-based), Render Cron Jobs run a command inside a container. The job exits when the command returns, and the exit code determines success or failure. Render retains logs for each run in the dashboard. The maximum duration for a single run is 12 hours; beyond that, Render terminates the job.1 Because each run starts a fresh container with no shared state from the previous invocation, any data that the job writes to the local filesystem is lost when the run exits.2

    Designing the command as a finite job

    Because Render executes a shell command rather than a route, your job should be idempotent and self-contained. It should know where to find files, credentials, and temporary output before it starts, because the container does not share state between runs and any data that is not written to an external store disappears when the job exits. Long-running work should write progress to stdout and exit with a non-zero status on failure so the dashboard and alerts reflect the real result.

    Assume the container is rebuilt from scratch on every invocation, because Render does not preserve the local filesystem between runs and there is no built-in retry if the command exits non-zero. Persist anything you need later to an external store before the process ends, and let the exit code carry the outcome rather than relying on side effects that vanish with the container. Treat the dashboard run history as your only durable record of what each scheduled run actually did.

    Common pitfalls

    All Render cron schedules use UTC, so convert your target local time before writing the expression. Using relative paths in the start command fails because the working directory is the repo root, not the directory of the script. A non-zero exit code marks the run as failed and triggers a notification if email or Slack alerts are configured.

    Treat the Render dashboard as the first place to inspect timing, duration, and exit status. If the schedule is correct but the command fails, move quickly from cron syntax to container environment, file paths, and dependency availability. That order prevents you from changing a valid expression while the real issue is inside the job.

    Reading Render run history

    The Render dashboard then becomes the first place to inspect timing, duration, and exit status for every run. Each row in the run history shows when the job started, how long it ran, and whether it exited cleanly or returned an error code. For jobs that run on a frequent schedule, filter the history to the last 24 hours so you can spot a recent failure without scrolling through weeks of successful runs. If a run shows a non-zero exit code, click into the log output to see the exact error message the command produced.

    Connecting Render cron jobs to external services

    Connecting your Render cron job to a database, object store, or external API requires only environment variables in the Render dashboard. A Python script that reads from a database, processes records, and writes results to S3 is a typical pattern: DATABASE_URL and AWS credentials live in Render environment variables, and the container image ships the script. The Render dashboard shows the exit code and runtime for each invocation, letting you confirm completion and check duration without any additional tooling.

    Timeout handling for upstream API calls

    For jobs that push data to a third-party API, add a request timeout inside your script that is shorter than Render's maximum run duration. Render terminates a job after 12 hours regardless of whether the upstream API is still responding. Setting a request timeout of 20 minutes ensures your script exits cleanly and logs the failure rather than being forcibly killed mid-operation.

    Paid tier behavioral differences

    Render Cron Jobs are billed per second of active running time with a minimum monthly charge of $1 per cron job service. Render provisions a fresh container for each run, so a few seconds of startup time is added before your command begins. For a nightly backup that starts at 2 AM, the startup delay is irrelevant; for a polling job that must respond within seconds, provisioned instances with faster startup are available on higher-tier plans. Upgrading to a paid plan also increases the maximum run duration beyond the free tier limit, which is important for long-running ETL jobs or database migrations that need more than a few minutes to complete.

    Notes

    Render does not provide a built-in retry mechanism for failed cron jobs. Render guarantees at most one active run per cron job at a time; if the next scheduled run arrives while a previous one is still active, Render delays it. Monitor runs in the Render dashboard under the job's "Logs" tab.

    Examples

    Daily database backup at 2 AM

    0 2 * * *

    Hourly health check

    0 * * * *

    Monday morning report at 8 AM

    0 8 * * 1

    Every 15 minutes (paid plans)

    */15 * * * *

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Daily database backup at 2 AM

    0 2 * * *
    Sources
    1. 1.

      Render, "Cron Jobs," render.com, accessed June 2026. https://render.com/docs/cronjobs

    2. 2.

      Render, "How Render handles scheduled tasks," render.com, accessed June 2026. https://render.com/articles/how-render-handles-scheduled-tasks

    3. 3.

      The Open Group, "crontab," pubs.opengroup.org, 2008. https://pubs.opengroup.org/onlinepubs/9699919799.orig/utilities/crontab.html

    FAQ

    Airflow DAG Schedule Interval

    Airflow turns a cron expression into data intervals, not just command triggers.1 The schedule parameter (formerly schedule_interval) accepts cron expressions, timedelta objects, preset strings such as @daily, @hourly, and @weekly, and timetable plugins.2

    Crucially, Airflow's execution model is offset-shifted: a DAG with schedule="@daily" and start_date=2024-01-01 first runs at the end of the first interval, meaning the earliest DAG run is 2024-01-02.2

    Syntax at a glance

    Set schedule='0 6 * * *' (or any 5-field cron expression) as a parameter to the @dag decorator or the DAG() constructor. Preset strings (@once, @hourly, @daily, @weekly, @monthly, @yearly, @continuous) map to common cron equivalents. None disables automatic scheduling. The catchup=False flag prevents Airflow from backfilling missed runs when the DAG is first enabled.

    Choosing an explicit schedule over a preset

    Presets are convenient when the schedule is truly standard, but an explicit cron expression makes the timing easier for teammates to audit. It also reduces ambiguity when you later review logs, backfill windows, or alerts, because the field values show the exact minute and hour instead of hiding behind a shorthand. Keep presets for obvious daily or hourly DAGs, and switch to an explicit cron expression when the run time, weekday range, or month boundary matters.

    Platform-specific differences

    In Airflow 2.4.0, schedule_interval was renamed to schedule and deprecated in the same release.1 Superseding the older execution_date context variable, data_interval_start and data_interval_end clarify that the run processes data for the period between those two timestamps, not the wall-clock time of execution.3 For timezone configuration, use pendulum.timezone() in the start_date argument, or set DEFAULT_TIMEZONE in airflow.cfg to apply a default across all DAGs.

    Reading the interval from the Airflow UI

    After the DAG is parsed, open the Graph or Grid view and inspect the data interval shown on each run. The interval is often more useful than the run timestamp because it tells you which slice of data the task was meant to process. When the interval and the task logic disagree, the schedule expression is probably not the only thing to review.

    The timezone you set also shapes the interval labels, so a DAG running in Europe/Paris shows different boundaries than one in UTC even when the cron string is identical. Pair the interval view with the Paused state and the start_date choice, because a DAG that looks correct in the Graph view can still be silent if it is paused or anchored to a start_date in the future. Reading the interval together with those three controls resolves most no-run investigations before you touch the expression.

    Common pitfalls

    Setting start_date=datetime.now() causes non-deterministic behavior across workers because the captured time differs each time the DAG file is parsed; always use a fixed past date like datetime(2024, 1, 1) so the schedule is identical on every worker. Forgetting catchup=False on a new DAG with a past start_date triggers dozens of backfill runs on activation, which can overwhelm a cluster that is already running production workloads. Using schedule_interval instead of schedule in Airflow 2.x causes a deprecation warning that will eventually become an error in future releases, so migrate to the schedule decorator early to avoid a last-minute fix during an upgrade.

    Backfill behavior and how to control it

    Setting catchup=True (the default in Airflow 2.x) tells Airflow to create a run for every missed interval between start_date and the current date the moment you enable the DAG.4 A daily DAG with a start_date six months in the past generates roughly 180 backfill runs on first activation, which the scheduler executes sequentially and can take hours to drain, delaying other DAGs from running.4

    Triggering intentional backfill separately

    For DAGs that do need historical data, trigger backfill intentionally after the DAG is stable with a bounded date range. This separates deliberate backfill from accidental backfill and avoids worker saturation. For all other DAGs, set catchup=False in the decorator or constructor as a default practice. Document the expected first run date in the DAG comment so reviewers can verify the offset behavior during code review. A comment like # first run: 2024-01-02 next to schedule='@daily' and start_date=datetime(2024, 1, 1) confirms the offset is intentional rather than a bug.

    Debugging a DAG that is not scheduling

    When an Airflow DAG is not creating new runs, check the DAG in the Airflow UI for the Paused toggle: a paused DAG does not schedule even when its expression is valid. After unpausing, the scheduler may take up to one minute to create the next run depending on the heartbeat interval.

    If the DAG remains paused-looking with no new runs, check for parse errors with the Airflow DAG report command for that DAG id. This command shows the last successful parse time and any import errors. A DAG file that fails to import due to a dependency error or syntax mistake is silently skipped by the scheduler; the parse error only appears in the scheduler logs or in the report command output.

    Notes

    Use the Airflow next-execution command to check the next scheduled run time. Set catchup=False in the DAG definition unless you explicitly want backfill behavior. Airflow uses the UTC timezone by default; set pendulum.timezone("America/New_York") in start_date for local time scheduling.

    Examples

    Daily at 6 AM UTC

    @dag(schedule='0 6 * * *', start_date=datetime(2024, 1, 1), catchup=False)

    Every weekday at midnight

    @dag(schedule='0 0 * * 1-5', start_date=datetime(2024, 1, 1), catchup=False)

    Hourly preset

    @dag(schedule='@hourly', start_date=datetime(2024, 1, 1), catchup=False)

    Manual-only (no schedule)

    @dag(schedule=None, start_date=datetime(2024, 1, 1))

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    Daily at 6 AM UTC

    @dag(schedule='0 6 * * *', start_date=datetime(2024, 1, 1), catchup=False)
    Sources
    1. 1.

      Apache Software Foundation, "DAGs — Airflow 2.4.0 Documentation," airflow.apache.org, accessed June 2026. https://airflow.apache.org/docs/apache-airflow/2.4.0/concepts/dags.html

    2. 2.

      Apache Software Foundation, "DAGs — Airflow 3.2.2 Documentation," airflow.apache.org, accessed June 2026. https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html

    3. 3.

      Ash Berlin-Taylor, "AIP-39 Richer scheduler_interval," cwiki.apache.org, October 2021. https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-39+Richer+scheduler_interval

    4. 4.

      Prepare.sh, "Scheduling, intervals & catchup in Airflow," prepare.sh, accessed June 2026. https://prepare.sh/tutorial/scheduling-intervals-catchup-in-airflow

    FAQ

    Cloudflare Workers Cron Trigger

    For Workers that need scheduled work without a separate scheduler, Cloudflare Cron Triggers turn a cron expression in wrangler.toml into a scheduled event. Standard 5-field syntax applies, and all schedules run in UTC.1 The free Workers plan includes three Cron Triggers per Worker; the Paid plan increases this to five per Worker with a minimum interval of one minute.2

    Syntax at a glance

    Add a [[triggers]] section to wrangler.toml with a crons array of strings: crons = ["0 * * * *"].1 In your Worker, export a scheduled handler alongside fetch: export default { fetch(req, env) {}, scheduled(event, env, ctx) {} }. The event.scheduledTime property contains the Unix timestamp of the trigger.3 The standard cron operators *, /, -, and , all work. You can define multiple cron expressions in the same crons array, and each one fires independently, so a single Worker can run different schedules without creating separate services. The wrangler.toml file must be valid TOML, so the crons array uses bare strings without quotes around the expression and commas between each entry.

    Platform-specific differences

    Unlike traditional cron, Cloudflare Workers run in a V8 isolate with no filesystem access, no shell, and a maximum CPU time of 30 seconds per invocation on the Paid plan.2 The waitUntil() method on the context object extends the Worker's lifetime for async work beyond the initial handler return. Cloudflare does not guarantee exact millisecond timing; triggers fire within a few seconds of the scheduled minute.

    Matching the work to Worker limits

    A cron trigger is a good fit for short, bounded jobs: flushing queues, refreshing small caches, checking health endpoints, or writing lightweight aggregates. It is not a replacement for a long-running batch worker. If the job needs a filesystem, a local shell, or more than the plan's CPU budget, move the heavy work to a service that can run to completion.

    Because the isolate has no persistent disk, anything your scheduled handler writes must go to a binding such as KV, R2, or D1 rather than a local file. The few-seconds timing slack also means a cron trigger is poor fit for work that must land on an exact second, so treat it as a periodic nudge and let the handler decide what to do when it fires. Match the job size to the plan budget, because a trigger that exceeds the CPU ceiling is stopped whether or not its async work has finished.

    Common pitfalls

    Defining only a fetch handler without a scheduled handler means the cron trigger fires but your Worker has no handler for it and silently does nothing. The free tier allows a maximum of 3 Cron Triggers; adding a fourth causes a wrangler deploy error.4 Missing await ctx.waitUntil(asyncTask()) causes async operations to terminate before completion.

    Checking both config and handler

    Before deployment, verify both the wrangler.toml trigger and the exported scheduled handler. One without the other is a common source of confusion because the platform may accept the configuration while the Worker still performs no scheduled work. Keep a small deployment checklist near the Worker source so the next maintainer checks both files together. Add a short comment above the wrangler.toml entry when the expression is tied to a business deadline. That comment helps reviewers distinguish intentional timing from accidental drift. It also gives future maintainers context when the schedule is touched months later.

    Using ctx.waitUntil for async cron work

    For every async operation in a Cloudflare Worker's scheduled handler, wrap the Promise with ctx.waitUntil(). Without it, the Worker runtime considers the handler complete as soon as the scheduled function returns, even if promises are still pending. An async fetch call that writes results to KV storage will be terminated mid-flight if you omit ctx.waitUntil().

    The correct pattern is ctx.waitUntil(doAsyncWork(env)). Your async function returns a Promise, and ctx.waitUntil extends the Worker's lifetime until that Promise resolves or rejects. Calling ctx.waitUntil multiple times with independent promises is valid; the runtime waits for all of them before closing the isolate. Use this fan-out pattern for cron work that writes to multiple storage bindings in parallel.

    Cron trigger limits and the Paid plan

    The Cloudflare Workers free plan allows up to three unique cron expressions per Worker. Each expression counts separately even if two expressions produce identical schedules on certain days, so a crons array with ["0 * * * *", "0 * * * *"] still counts as two triggers rather than one. Upgrading to the Paid plan removes this per-Worker cap entirely and also increases the minimum interval from once per day to once per minute, which is essential for jobs that need near-real-time polling.

    CPU time and billing for scheduled handlers

    Cron triggers consume your Worker's CPU time allocation, not its request count, so a busy cron schedule can exhaust your CPU budget before your fetch traffic does. On the Paid plan, CPU time for cron runs is billed separately from fetch handler usage, and the CPU ceiling rises from 10 milliseconds to 30 seconds per invocation. For Workers that perform heavy computation in scheduled handlers (image processing, large data transformations), monitor the CPU time logged in the Cron Triggers dashboard to stay within your plan's limits.

    Notes

    Test Cron Triggers locally with wrangler dev in scheduled mode and invoke the local scheduled endpoint. Cloudflare logs the exit status of each trigger run in the Workers dashboard under "Cron Triggers". Use ctx.waitUntil() for any async work that must complete after the handler returns.

    Examples

    wrangler.toml entry

    [triggers]
    crons = ["0 0 * * *"]

    Every 5 minutes

    crons = ["*/5 * * * *"]

    Scheduled handler (TypeScript)

    export default {
      async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
        ctx.waitUntil(doWork(env));
      }
    };

    Verify with the Cron Expression Parser & Next-Run Preview tool.

    wrangler.toml entry

    [triggers]
    crons = ["0 0 * * *"]
    Sources
    1. 1.

      Cloudflare, "Cron Triggers · Cloudflare Workers docs," developers.cloudflare.com, accessed June 2026. https://developers.cloudflare.com/workers/configuration/cron-triggers/

    2. 2.

      Cloudflare, "Limits · Cloudflare Workers docs," developers.cloudflare.com, accessed June 2026. https://developers.cloudflare.com/workers/platform/limits/

    3. 3.

      Hrishik Shukla, "Schedule Cloudflare Worker using Cron Triggers," dev.to, accessed June 2026. https://dev.to/hrishiksh/schedule-cloudflare-worker-using-cron-triggers-2glp

    4. 4.

      Aaron Lisman, "Making Time for Cron Triggers: A Look Inside," blog.cloudflare.com, September 2020. https://blog.cloudflare.com/cron-triggers-for-scheduled-workers/

    FAQ