How to Run a cron Job Every Two Weeks / Months / Days

We may want to run some jobs for every two weeks/months/days… under some situation such as backing up for every other week. In addition, we may add more complex rules for running jobs, e.g. run a command when the load of the server is higher than a certain level. With the help of the shell programming language we can easily achieve this goal.

Basics of the crontab

Crontab file controls how and what to run by cron. The crontab line has a format as follows:

.---------------- minute (0-59)
|  .------------- hour (0-23)
|  |  .---------- day of month (1-31)
|  |  |  .------- month (1-12) OR jan,feb,mar,apr ...
|  |  |  |  .---- day of week (0-6) (Sunday=0 or 7) OR sun,mon,tue ...
|  |  |  |  |
*  *  *  *  *  command to be executed

To add a cron job for your current user, run

$ crontab -e

And edit the crontab file by adding/deleting lines. Check the crontab man page for more details.

The cron job is run as a piece of shell code. /bin/sh is the shell by default. You may change it to some other shells like bash by adding a like like SHELL=/bin/bash at the beginning of the crontab file.

Method 1: use crontab extensions

crontab provides some extensions to support this: ranges can include “steps” like /2. 1-9/2 is the same as 1,3,5,7,9. This can be the easiest method if the job is intended to be run every two N in a range such as N is “day” and the range is “days in a month”. Check the crontab man page for more details about ranges and steps of crontab.

An example.

To run the command every two months at 1:00 on the first day of the month, the crontab is

0 1 1 */2 * command to be executed

But please be aware that the every two N here is not exactly every two N. For days, it is every two days in a month. For months, it is every two months in a year. If you need to make sure your cron job is run exactly every two N, check the other methods.

Method 2: use a test statement to control whether to really run the command

The command executed for a cron job is a piece of shell code. So you can make the command run every day/week/month while use a test statement to control whether to really run the command.

Let’s take an example to illustrate the idea. Now, we want to run a job every 2 days. As there are months having 31 days, some jobs are not run exactly every 2 days if we use method 1 above. We can have a cron job as follows to make sure it runs exactly every 2 days (86400 seconds) at 1:00.

0 1 * * * [[ $((($(date +%s) / 86400) % 2)) == 0 ]] && /path/to/your/command

Here, the shell script [[ $((($(date +%s) / 86400) % 2)) == 0 ]] && /path/to/your/command is run every day. But your command is run only when $((($(date +%s) / 86400) % 2)) == 0. That is when the number of days since 1970-01-01 00:00:00 UTC is divisible by 2.

With the help of date, we can make make test statements. Let’s take another example, run a job exactly every 5 months starting from March of 2016. The cron tab entry can be

0 1 1 * * [[ $(((($(date +%Y) - 2016) * 12 + $(date +%m) - 3) % 5)) == 0 ]] && /path/to/your/command

Only when the number of months since March 2016 is divisible by 5, the command is run.

Method 3: use a shell script with its state saved on disk

The idea is to invoke a shell script every day/week/month and the shell judges whether to run the job. The key idea here is that the shell uses a file to mark its state.

The shell code:

#!/bin/bash

# a file marking the state on disk
mark_file=$HOME/.job-run-marker-1

# check whether the job run last time it is invoked
if [ -e $mark_file ] ; then
    rm -f $mark_file
else
    touch $mark_file
    exit 0
fi

# job command is here

The script will not find $mark_file on the first run, so it will create it and exit. On the second run the script will remove $mark_file and then proceed to execute the job command. For the third run, it is the same as the first run. So if this script is run weekly by cron, the job command will run every two weeks.

You can extend the shell with more complex logic like storing the last run date/time in the $mark_file or check more system information like the CPU/mem/disk load.

An example is as follows.

We want to back up Xen DomU VMs for every two weeks. We create a script /home/share/bin/xen-bak. The beginning part of this script is like what we list above.

The line for the job is as follows.

0 2 * * 2 /home/share/bin/xen-bak

The backup command will run at 2:00 for every other Tuesday.

Eric Zhiqiang Ma

Eric is interested in building high-performance and scalable distributed systems and related technologies. The views or opinions expressed here are solely Eric's own and do not necessarily represent those of any third parties.

8 comments:

  1. Wouldn’t 0 2 * * 2 just run it 2am EVERY Tuesday?
    I think what your looking for is:
    0 2 * * 2/2
    or
    0 2 * * Tue/2

    1. You are right. The most updated instruction is at the beginning of the post.

      The “poor man’s” solution here works actually — the invoked script is invoked every Tue but it records its state and does nothing every next time it is invoked.

      1. Your explanation defeats the purpose of CRON. I could implement my own logic in code (every two weeks) and I only need cron to run every minute … So please correct your script.

  2. This is easy to do in your crontab file with no external scripting:

    0 2 * * tue [ `expr $(date ‘+\%U’`) % 2` -eq ‘0’ ] && /home/share/bin/real-xen-bak

    This will only work if your “date” command understands the “%U” format, which gives the week-of-the-year. The “date” command on most *NIX systems will understand this, as “date” uses the C-library’s “strftime” function which specifies “%U” as the format specifier for week-of-year.

  3. I was wondering if there was a way to schedule something, say every 5 months, that changes the month when it crosses a year boundary. For example I have the following cron expression :

    0 1 1 15 3/5 ? *

    Which, when I run it starting at the beginning of the year gives me these fire times :
    1. Sunday, March 15, 2015 1:01 AM
    2. Saturday, August 15, 2015 1:01 AM
    3. Tuesday, March 15, 2016 1:01 AM
    4. Monday, August 15, 2016 1:01 AM
    5. Wednesday, March 15, 2017 1:01 AM

    Notice that if it were truly scheduling every 5 months, starting on the 3rd month, I would expect it to schedule in March 2015, August 2015, January 2016, June 2016, etc

    Is this not the correct cron expression, or is there something I’m missing?

    1. Hi Quenten, that’s a good question. The ‘*/5’ for months in crontab means every 5 months in a year.

      For your purpose, you may try:

      0 1 1 * * [[ $(((($(date +%Y) - 2016) * 12 + $(date +%m) - 3) % 5)) == 0 ]] && /path/to/your/command
      

      I also updated the post with more details. Please check out.

Leave a Reply

Your email address will not be published. Required fields are marked *