0 9,21 * * * runs a job twice a day, at 09:00 and 21:00. A comma makes a list, so the hour field 9,21 picks exactly those two hours, and the 0 in the minute field runs each one on the hour.
When it fires
Checked at 09:58 UTC on October 14, that morning’s run has passed, so the next five are:
Oct 14 21:00 Oct 15 09:00 Oct 15 21:00 Oct 16 09:00 Oct 16 21:00
Any two hours work the same way: 0 0,12 * * * is midnight and noon, the same schedule as 0 */12 * * *, and 0 8,17 * * 1-5 is the start and end of a working day, Monday to Friday.
A comma, not a dash
0 9-21 * * * is a range: it runs every hour from 09:00 to 21:00, 13 times a day. One character turns two runs into thirteen, so read the hour field twice before saving.
Two different minutes
The fields combine as a grid: every listed minute in every listed hour. So 0,30 9,21 * * * doesn’t mean 09:00 and 21:30. It runs four times, at 09:00, 09:30, 21:00 and 21:30. For two times with different minutes, write two lines:
0 9 * * * /usr/local/bin/digest.sh
30 21 * * * /usr/local/bin/digest.sh
If the two runs do different work, such as a morning summary and an evening cleanup, two lines with two commands are clearer anyway.
The same schedule elsewhere
- crontab:
0 9,21 * * * /usr/local/bin/digest.sh, in the server’s timezone unless the crontab setsCRON_TZ. - GitHub Actions:
cron: '0 9,21 * * *', in UTC. For different minutes, list two- cron:entries underon.schedule. - Kubernetes CronJob:
schedule: "0 9,21 * * *". A CronJob takes one schedule, so two different minutes need two CronJobs. - AWS EventBridge:
cron(0 9,21 * * ? *). - systemd timer:
OnCalendar=09,21:00, whichsystemd-analyze calendarnormalizes to*-*-* 09,21:00:00. A timer takes severalOnCalendar=lines, so09:00and21:30fit in one. - Spring
@Scheduled:0 0 9,21 * * *.