Cron Expression: Every Other Day (Every 2 Days)
0 0 */2 * * runs at midnight on odd days of the month (1, 3, 5 …). It looks like “every other day,” but */2 restarts at day 1 each month, so a 31-day month puts runs on the 31st and the 1st back-to-back. Here is the caveat and a drift-free workaround.
What 0 0 */2 * * actually does
*/2 in the day-of-month field means “every 2nd day starting from day 1” — so it fires on the 1st, 3rd, 5th, …, 29th, 31st. That is genuinely every other day within a month, and for most jobs it is fine.
The month-boundary catch
Because the counter resets on the 1st of every month, a 31-day month runs on the 31st and then again on the 1st — two days in a row. February (28 days) runs on the 27th then skips to the 1st — a three-day gap. If your job must be exactly 48 hours apart, this drifts.
Drift-free workaround: gate in the script
Run daily and let the script decide, using the Unix day number modulo 2:
0 0 * * * in cron, then at the top of the script: [ $(( $(date +%s) / 86400 % 2 )) -eq 0 ] || exit 0. The day-count never resets at month boundaries, so runs stay exactly 24×2 hours apart forever.
Which to use
Use 0 0 */2 * * when “roughly every other day” is good enough and you like a single crontab line. Use the daily + modulo gate when the 48-hour spacing must be exact across months.
FAQ
Does 0 0 */2 * * run exactly every 48 hours?
No. */2 counts from day 1 of each month, so at month boundaries you can get two runs on consecutive days (e.g. the 31st and the 1st) or a longer gap after a short month.
How do I get a true every-48-hours schedule?
Run 0 0 * * * daily and gate the script with [ $(( $(date +%s) / 86400 % 2 )) -eq 0 ] || exit 0, which uses the continuous Unix day count and never resets.
What does */2 mean in cron?
It is a step value: start at the field minimum and fire every 2nd value. In day-of-month that is days 1, 3, 5, and so on.