Page Body

Scheduling Tasks With GNU/Linux

There are several ways to schedule tasks on a GNU/Linux system.

Note: If you are not familiar with the GNU/Linux command line interface, review the Conventions page before proceeding.

systemd Timers

The systemd initialization system offers timer and service units to schedule tasks. In a .timer unit, a job schedule is defined and the service unit that should be activated is set. The .service unit is used to set the actual command to be executed.

A systemd timer can be:

  1. Monotonic
  2. Realtime

Monotonic Timers

A monotonic timer schedules execution of a task a specific amount of time after a predefined event takes place. There are several keywords that can be used in the [Timer] section of the .timer unit to delineate this time.

Relatively schedule the task to the time when the timer unit itself is activated.
Relatively schedule the task to the system boot time.
Relatively schedule the task to the time when systemd started.
Relatively schedule the task to the last time the service unit was active.
Relatively schedule the task to the last time the service unit was inactive.

Seconds are used as the default unit of time. However, you can specify a different time unit after the numerical value (e.g., 10m for ten minutes).

Realtime Timers

A realtime timer schedules execution of a task in absolute terms. The OnCalendar keyword and specific allowed time encodings are used to delineate this time.

A list of values can be given as a comma-separated list (e.g., 09:00:00,15:00:00). A range of values can be specified using .. (e.g., Monday..Friday). A * character is a wildcard and matches any value.

ddd hh:mm:ss
The task will be executed each ddd at hh:mm:ss, e.g., Mon 09:00:00.
ddd..ddd *-MM-dd
The task will be executed the dd of MM of each year, but only on days from ddd to ddd, e.g., Mon..Fri *-06-01.
The task will be executed on the dd of MM of the year yyyy at 00:00:00, e.g., 2020-06-01.
ddd,ddd yyyy-*-dd,dd hh:mm:ss
The task will be executed at hh:mm:ss of the dd and dd day of each month of the year yyyy, but only if the day is ddd or ddd, e.g., Mon,Tue 2020-*-03,09 09:25:00.
The task will be executed every mm minutes starting from minute mm, e.g., *:00/04.
The task will be executed every hh hours starting from hh, e.g., 19/01.
The task will be executed at the beginning of each minute.
The task will be executed at the beginning of each hour.
The task will be executed each day at 00:00:00.
The task will be executed the first day of each month at 00:00:00.

If provided, weekdays must be in English, either as the abbreviated (e.g., Sun) or complete form (Sunday).

Run man 7 systemd.time for more information.

Viewing Active Timers

Active .timer units can be viewed with the following command:

systemctl list-timers

The output includes six columns:

  1. NEXT The next time the timer will run.
  2. LEFT How much time before the timer will run again.
  3. LAST The last time the timer ran.
  4. PASSED How much time has passed since the last time the timer ran.
  5. UNIT The .timer unit in which the schedule is set.
  6. ACTIVATES The .service unit activated by the timer.


Assume there is a script in our user's Script directory called fortune.bash. We want this script to run daily.

#!/usr/bin/env bash
# Author:
# Purpose: Print out today's fortune.

fortune > "${HOME}/fortune.txt"

exit 0  # Exit script with successful exit status

The script is simple. It prints out a fortune using the eponymous fortune command and directs the command's output to a fortune.txt file in the user's home directory (fortune is likely not installed on your GNU/Linux distribution, but should be available in the distribution's repository, e.g., # apt install fortune).

For this example, the job in question only needs to be run for a single user, so it is advantageous for that user to be able to create and manage the job without requiring root privileges. systemd offers this option via user services.

First, make sure that a ${HOME}/.config/systemd/user/ directory exists on your system. If it does not, create it with the following command:

mkdir -p "${HOME}/.config/systemd/user/"

Next, create the .timer file:

$ {
    echo '[Unit]'
    echo -e 'Description=Announce daily fortune\n'
    echo '[Timer]'
    echo 'OnCalendar=daily'
    echo -e 'Persistent=true\n'
    echo '[Install]'
    echo ''
} > "${HOME}/.config/systemd/user/fortune.timer"
  • The Description= option specifies a human readable name for the unit.
  • OnCalendar defines realtime timers with calendar event expressions (in this case, to operate daily).
  • Persistent= takes a boolean argument. If true, the time when the service unit was last triggered is stored on disk. When the timer is activated, the service unit is immediately triggered if it would have been triggered at least once during the time when the timer was inactive. This helps to catch up on missed runs of a service when the computer was off.
  • WantedBy= ensures symbolic links are created in the .wants/ or .requires/ directory of each of the listed units when this unit is installed by systemctl enable. This has the effect that a dependency of type Wants= or Requires= is added from the listed unit to the current unit. The result is that the current unit will be started when the listed unit is started (i.e., fortune.timer is started when is started).

Then, create the corresponding fortune.service file:

$ {
    echo '[Unit]'
    echo -e 'Description=Run fortune.bash script\n'
    echo '[Service]'
    echo "ExecStart=/home/${LOGNAME}/Scripts/fortune.bash"
} > "${HOME}/.config/systemd/user/fortune.service"
  • ExecStart= contains commands with their arguments that are executed when this service is started. Unless Type= is set to oneshot, exactly one command must be given. If ExecStart= is specified, but neither Type= nor BusName= is specified, the default type is automatically set to simple (i.e., systemd will consider the unit immediately started after the main service process has been forked off).

Enable and start the fortune.timer unit:

$ systemctl --user enable --now fortune.timer
Created symlink /home/amnesia/.config/systemd/user/ → /home/amnesia/.config/systemd/user/fortune.timer.

Confirm that systemd sees the active timer:

$ systemctl --user list-timers
NEXT                         LEFT     LAST PASSED UNIT          ACTIVATES
Wed 2020-09-02 00:00:00 PDT  15h left n/a  n/a    fortune.timer fortune.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

Check the statuses of the new units:

$ systemctl --user status fortune.timer fortune.service
● fortune.timer - Announce daily fortune
   Loaded: loaded (/home/amnesia/.config/systemd/user/fortune.timer; enabled; vendor preset: en
   Active: active (waiting) since Tue 2020-09-01 08:51:38 PDT; 1min 36s ago
  Trigger: Wed 2020-09-02 00:00:00 PDT; 15h left

● fortune.service - Run fortune.bash script
   Loaded: loaded (/home/amnesia/.config/systemd/user/fortune.service; static; vendor preset: e
   Active: inactive (dead)

Now, a new fortune should be placed in /home/amnesia/fortune.txt every day at 00:00:00.

If you change the fortune.timer file to move to hourly fortunes, make sure to run the systemctl --user daemon-reload command, so that systemd reloads its unit files.

systemd Timer Script

The steps required to manually create a systemd timer can be onerous. A Bash script can automate this process.

First, the script ensures that the systemd user unit directory exists. If it does not, it creates it.

# Create systemd user unit directory, and parent directories, if they do not exist
if ! "${HOME}/.config/systemd/user" > '/dev/null' 2>&1; then
    mkdir -p "${HOME}/.config/systemd/user"

Afterwards, the script outputs a link to a useful systemd directives reference and collects the information needed from the user to create basic timer/service unit files:

# Output systemd directives reference
echo -e '\nsystemd Directives Reference\n'

# Get unit name
echo ''
read -p 'Enter unit name: ' -r unit_name
unit_name="$(clean_name "${unit_name}")"  # Sanitize input

# Gather .timer unit description
read -p 'Enter .timer description: ' -r timer_description
timer_description="${timer_description//[^a-zA-Z0-9 ,]/}"  # Sanitize input

# Gather .timer unit directive
read -p 'Enter .timer directive: ' -r timer_directive
timer_directive="${timer_directive//[^a-zA-Z0-9 =]/}"  # Sanitize input

# Gather .service unit description
read -p 'Enter .service description: ' -r service_description
# Sanitize input
service_description="${service_description//[^a-zA-Z0-9 ,]/}"

# Gather .service unit command
read -p 'Enter .service command: ' -r service_command

Then, two different functions are called that contain the commands that actually create the .timer and .service files in the appropriate location:

timer_matter  # Create .timer unit

service_matter  # Create .service unit

After the unit files are created, the user is given the option to edit the unit files in their chosen unit editor:

while true; do
    # Present option to open units in editor
    echo ''
    read -p 'Open units in editor? (y or n): ' -r edit

    if [[ "${edit}" == 'y' || "${edit}" == 'n' ]]; then
    elif [[ "${edit}" == 'q' ]]; then
        exit 0
        err_msg='\nInvalid input. Try again or enter q to quit.'
        echo -e "${err_msg}" 1>&2

Finally, the .timer unit is enabled and activated, and the units' status is output:

# Enable and start units
systemctl --user enable --now "${unit_name}.timer"

# Check units' status
systemctl --user status "${unit_name}.timer" "${unit_name}.service"

The full script is available below:


The most well-known way of scheduling tasks on a GNU/Linux system is via the cron system. The systemd initialization system does support cron, as well as the .timer unit method.

cron jobs are run by crond. The cron daemon's purpose is to execute jobs at periodic intervals. crond should be automatically started during system boot. No action is required on your part.

crond reads its task lists once on starting and then keeps them in memory. However, the daemon checks every minute whether any crontab files have changed. The mtime (i.e., the last modification time) is used for this. If crond notices a modification, the task list is automatically reconstructed. In this case, no explicit restart of the daemon is required.

cron jobs are stored in a variety of crontab (cron table) files.

The basic syntax for the crontab command is:

crontab ex_file

A new crontab will be installed from ex_file. Alternatively, a new crontab can be installed from the standard input by passing a - as an argument to crontab instead of ex_file.

You can remove your user's crontab file with the -r option:

crontab -r

User crontabs

User crontabs are stored in either /var/spool/cron/crontabs/ (Debian) or /var/spool/cron/ (Fedora). These files are managed with the crontab command.

For example, you can view your user's crontab file with the crontab -l command. If you have root access, you can view any user's crontab file like so:

# crontab -l -u ex_username

To edit your crontab file, use the crontab -e command. As with crontab -l, if you have root access, you can edit any user's crontab file:

# crontab -e -u ex_username


A crontab line consists of six columns (fields):

  1. Minute (0-59)
  2. Hour in 24 hour time (0-23)
  3. Day of month (1-31)
  4. Month (1-12, or names, e.g., dec)
  5. Day of week (0-7, with 0 and 7 being Sunday, or names, e.g., sun)
  6. Command

To match all values for a column, you can use an asterisk (*).

This is an example crontab line that runs a shell script at 20:30 every Saturday:

30 20 * * 6 '/home/amnesia/Scripts/'

Multiple times (e.g., minutes, hours, or months) can be specified by separating the items with a comma (,), a range can be specified with a hyphen (-), and both of these methods can be combined. Ranges and lists of names for months and weekdays are not permissible.

You can specify that a job run every N units of time by creating a step with the forward slash (/), e.g., */2.

If you specify both the day of the month and the day of the week, the job will run with either match. Otherwise, all columns must match up for the job to run.

The month and day of week names can be spelled out using the first three letters of each value (e.g., dec, sun). The whole does not need to be consistent. Numbers or names can be used at your convenience.

The command you run can be any valid shell code. Percent signs (%) within the command must be escaped \% or they will be converted into newline characters. In that case, the command will be considered to extend up to the first (unescaped) percent sign. The following lines will be fed to the command as its standard input.

If you do not want a cron command execution to be logged, you can suppress this by putting a - as the first character of the line. The final line of a crontab file must end in a newline character. Otherwise, it will be ignored.


cron has its own minimal environment. The cron environment can be set at the top of the crontab file, just like you would do in any other configuration file.

The following variables are automatically pre-set:

The home directory is taken from /etc/passwd. Changing its value is allowed.
The user name is taken from /etc/passwd and cannot be changed.
crond sends email containing command output to this address (by default, they go to the owner of the crontab file). If crond should send no message at all, the variable must be set to a null value, i.e., MAILTO=''.

CRON_TZ is a special crontab environment variable. It used to set an alternate time zone used for the crontab. By default, system time is used.

Anything a cron job prints to the screen is sent to the current user in an email. This can be overridden with the MAILTO variable.


A crontab nickname replaces the five time columns in a crontab line.

Same as @yearly.
Run once a day at midnight, 0 0 * * *.
Run once an hour, on the hour, 0 * * * *.
Same as @daily.
Run at midnight on the first of the month, 0 0 1 * *.
Run once, at startup.
Run once a week on Sunday at midnight, 0 0 * * 0.
Run once a year at midnight on January 1st, 0 0 1 1 *.

System crontabs

The system crontab is located at /etc/crontab.

Package crontabs that are installed when you install packages are located in the /etc/cron.d/ directory.

All of these crontab files are considered system crontabs. These crontabs have an extra column before the command column that indicates which user should be used to execute the cron job.

Convenience crontabs

The convenience crontab directories are located in the /etc/ directory. They are:

  • /etc/cron.daily/
  • /etc/cron.hourly/
  • /etc/cron.monthly/
  • /etc/cron.weekly/

Files placed into these directories are normally scripts and they do not necessarily run on a predictable schedule (i.e., you cannot know exactly when a script will run within the directories' allotted timeframe). Most of the files in these directories are placed there by installation scripts.

Access Control

The users that can use cron jobs can be restricted by using either the /etc/cron.allow or /etc/cron.deny file. If /etc/cron.allow exists, only users in the file and the root user can use the crontab command. If /etc/cron.deny exists, users in the file cannot use the crontab command.

If both the /etc/cron.allow and /etc/cron.deny files exist, /etc/cron.allow takes precedence. However, you should only use one of these files.

If neither file exists, the GNU/Linux distribution's default settings will determine whether or not a user can use the crontab command.


anacron is a simplified cron that is meant to handle jobs that run daily or less frequently (e.g., weekly, monthly), and jobs for which the precise time does not matter. anacron runs jobs that were scheduled to run when the system was off.

At most, anacron is activated once a day. For anacron, the time of day is irrelevant. Instead, it focuses on when the last time a particular job was run.

There are no commands to edit anacron jobs analogous to crontab -e in the cron system. All jobs are run as the root user and only the system administrator can handle anacron job management.

The anacron table is located at /etc/anacrontab. If you need to edit this file, use the sudoedit command:

sudoedit '/etc/anacrontab'

anacron can run as a daemon, but it is typically run from cron itself to check if there are any outstanding jobs. Once it is done processing, anacron exits.

How anacron runs its jobs can be modified with the anacron command. To manually run an anacron job, pass its job identifier to the anacron command as an argument:

# anacron ex_job_identifier...

ex_job_identifier can be a shell search pattern. This can be used to launch groups of jobs with a single anacron invocation. Not specifying any job names at all is equivalent to the job name *.

Normally, anacron executes jobs independently and without attention to overlaps. # anacron -s serializes the execution of jobs, such that anacron starts a new job only when the previous one is finished.

Unlike cron, anacron is not a background service, but is launched when the system is booted in order to execute any leftover jobs. Later on, you can execute anacron once a day from cron in order to ensure that it completes its tasks, even if the system is running for a longer period of time than normally expected.

GNU/Linux systems ensure that anacron does nothing while cron is active. For example, /etc/cron.daily/ contains a script called 0anacron, which is executed as the first job.

This script invokes anacron -u, which causes anacron to update the time stamps of jobs without actually executing any jobs (which is the next thing that cron will accomplish). When the system is restarted, this will prevent anacron from running jobs unnecessarily, at least if the reboot occurs after cron has completed its tasks.


An anacron line consists of four columns (fields):

  1. Period, in days, between runs of this job. Some @nicknames are available (e.g., @annually, @daily, @monthly, @weekly, @yearly).
  2. Delay, in minutes, that anacron will wait before executing the job.
  3. A tag, called a Job Identifier, for the the job that is unique. anacron uses this to identify the job in log messages and as the name of the file in which anacron logs the time the job was last executed. It can contain any characters, except white space and the forward slash.
  4. The command to run.

Long lines can be wrapped with a \ at the end of the line.


The anacron environment can be set just like in a normal crontab file. Variable definitions are valid until the end of the file or until the same variable is redefined.

Some environment variables have a special meaning to anacron. For example, with RANDOM_DELAY, you can specify an additional random delay for the job launches. When you set the variable to a number t, a random number of minutes between 0 and t will be added to the delay given in the job description.

START_HOURS_RANGE lets you denote a range of hours (on a clock) during which jobs will be started (e.g., START_HOURS_RANGE=11-12 allows new jobs to be started only between 11:00 and 12:00).

Like cron, anacron sends job output to the address given by the MAILTO variable. Otherwise, it sends job output to the user executing anacron (usually the root user).

at and batch

The at service is used to execute shell commands one time at a point in the future (versus cron, which is preferable for commands that need to be repeatedly executed). The format of the command is:

at ex_time_specification...

After entering the above, you will be presented with an at> prompt where you can enter the shell command to schedule. To exit the at> prompt and schedule the job, press Ctrl+d. Afterwards, you will a see a line that echoes the job's scheduling information.

The at command also accepts input via a pipe or redirection:

echo ex_command | at ex_time_specification...

at ex_time_specification... < ex_file.txt

at can read commands from a file using the -f ex_file option, as well.

A summary of your user's at queue can be shown with either of the following commands:

at -l


An a in the output denotes a job class, i.e., a letter between a and z. You can specify a job class using at's -q ex_queue option. Jobs in classes with later letters are executed with a higher nice value.

The default is a for at jobs and b for batch jobs. A job that is currently being executed belongs to the special job class =.

To show a summary of all users' at queues, you can use one of the following commands:

# at -l

# atq

The commands that make up a job can be verified with the -c option:

at -c ex_job_number...

An at job can be deleted with the following commands:

at -d ex_job_number...

atrm ex_job_number...

The batch command is similar to at, but makes it possible to execute a command as soon as possible. When that will be depends on the system load, i.e., if the system is busy, batch jobs will need to wait.

An at-style time specification on batch is allowed, but not required. If it is given, the commands will be executed some time after the specified time, just as if they had been submitted using the batch command at that time.

batch is not suitable for environments in which users compete for resources (e.g., like CPU time).

To schedule a one-time job once the system's 1 minute load average is less than or equal to the system default value of 0.8 (i.e., in the last minute, 80% of processes or less were waiting to be run), or whatever limiting load factor was specified by atd, you can do:

echo ex_command | batch

at job execution is controlled by a daemon called atd. Generally, it is started on system boot and waits in the background for work.

When starting atd, several options can be specified:

-b (batch)
Determines the minimum interval between two batch job executions. The default is 60 seconds.
-d (debug)
Activates debug mode, i.e., error messages will not be passed to the logging system, but written to standard error output.
-l ex_integer (load)
Determines a limit for the system load, above which batch jobs will not be executed. The default is 0.8.
For a system with N processors, you want ex_integer to be slightly less than N, e.g., 80% of N. ex_integer is a load average value, i.e., a limiting load factor number over which batch jobs will not be run.

To immediately run all jobs queued by at, use the atd command.

The atd daemon requires the following directories, in which at and batch job files are located:

  • /var/spool/cron/atjobs/ (Debian) or /var/spool/at/ (Fedora). Their access mode should be 700 and their owner should be at.
  • /var/spool/atspool/. This directory serves to buffer job output. Its access mode should be 700 and its owner should also be at.

at job files are prefixed with an a. batch job files are prefixed with a b.


Specifiers are what are used to specify the time that at jobs should run.

You can specify any number of minutes, hours, days, and weeks from the current time, e.g., now, today, tomorrow, now +30 minutes.
Run the task at 00:00 on the current day.
Run the task at 12:00 on the current day.
Run the task at 16:00.
Such as 1:00 PM, 6:00 AM, or 13:00 090820, 13:00 09/08/20, or 13:00 09.08.20.

If you just specify a date (e.g., 09/08/20), commands will be executed on the day in question at the current time. If giving both a date a time, the date must come after the time.

at supports the units minutes, hours, days, and weeks. When using offsets, a single offset by one single measurement unit must suffice, i.e., the following combinations are not allowed, at noon + 2 hours 30 minutes or at noon + 2 hours + 30 minutes.


at tries to run the commands in an environment that is as like the one when at was called as possible. The current working directory, the umask, and the current environment variables (except _, DISPLAY, and TERM) are saved and reactivated before the commands are executed. Output of the commands executed by at (i.e., standard output and standard error) is sent to you via your user's email address.

Both at and batch run from the same environment that existed at the time the job was submitted (i.e., all variables, aliases, and functions in the shell are available to the job that was started in the shell). This is distinct from cron and anacron, which have their own minimal environments.

If you are running the at command as another user (e.g., through use of the su command), the commands will be executed using that identity. However, the output mail messages will still be sent to your account's email address.

Access Controls

at and batch have the same type of access control as cron does. The at and batch access control files are /etc/at.allow and /etc/at.deny. If neither file exists, at and batch are only available to the root user.


The sleep command is used to delay a command from running for a specified period of time.

sleep ex_integer_ex_suffix...

ex_suffix can be:

  • d for days
  • h for hours
  • m for minutes

If no suffix is specified, seconds is assumed.

If two or more arguments are provided, sleep pauses for the amount of time specified by the sum of the argument values.


For more on systemd timers and related resources, run the following commands:

man 1 systemd
Introduction to basic concepts and functionality of systemd.
man 1 systemctl
Command that controls the systemd system and service manager.
man 5 systemd.unit
Unit configuration information.
man 5 systemd.timer
Timer unit configuration.
man 5 systemd.service
Service unit configuration.

The ArchWiki also has a nice reference on systemd user services.

For more on cron, anacron, at, and batch, examine the Linux User's Manual, either at the command line or online.

Enjoyed this post?

Subscribe to the feed for the latest updates.