← All cron schedules

Cron first day of every month

The cron expression 0 0 1 * * runs a job at 00:00, on the 1st of the month. The next runs are 2026-11-01 00:00 and 2026-12-01 00:00 UTC. The shortest gap between runs is 28 days, so a heartbeat monitor with a 60-minute grace period catches a missed run.

0 0 1 * *
At 00:00, on the 1st of the month.
minutehourday of monthmonthday of week
001**

Next runs (UTC)

  1. Sun 2026-11-01 00:00 UTC (+00:00) · in 19d 4h
  2. Tue 2026-12-01 00:00 UTC (+00:00) · in 49d 4h
  3. Fri 2027-01-01 00:00 UTC (+00:00) · in 80d 4h
  4. Mon 2027-02-01 00:00 UTC (+00:00) · in 111d 4h
  5. Mon 2027-03-01 00:00 UTC (+00:00) · in 139d 4h
  6. Thu 2027-04-01 00:00 UTC (+00:00) · in 170d 4h
  7. Sat 2027-05-01 00:00 UTC (+00:00) · in 200d 4h
  8. Tue 2027-06-01 00:00 UTC (+00:00) · in 231d 4h
  9. Thu 2027-07-01 00:00 UTC (+00:00) · in 261d 4h
  10. Sun 2027-08-01 00:00 UTC (+00:00) · in 292d 4h

Notes for this schedule

Same as @monthly. If the job reports on 'last month', compute the month in the script instead of assuming the server's clock and timezone line up with your users'.

Examples

crontab (crontab -e)
0 0 1 * * /usr/local/bin/my-job.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>
systemd timer
# /etc/systemd/system/my-job.timer
[Unit]
Description=Run my-job (first day of every month)

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

[Install]
WantedBy=timers.target
and the service it starts
# /etc/systemd/system/my-job.service
[Unit]
Description=my-job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/my-job.sh
# runs only if ExecStart succeeded
ExecStartPost=/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>

Check the calendar spec with systemd-analyze calendar "*-*-01 00:00:00", then systemctl enable --now my-job.timer. Persistent=true runs a missed job at boot.

Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-job
spec:
  schedule: "0 0 1 * *"
  timeZone: "Etc/UTC"          # Kubernetes 1.27+; otherwise the controller's timezone
  concurrencyPolicy: Forbid       # don't start a new run while one is active
  startingDeadlineSeconds: 300    # still start if the controller was briefly down
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: job
              image: your-image:tag
              command: ["/bin/sh", "-c", "/app/run-job.sh && curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>"]
GitHub Actions
on:
  schedule:
    - cron: "0 0 1 * *"   # always evaluated in UTC
  workflow_dispatch: {}          # lets you run it by hand too

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./run-job.sh
      - name: Tell Vigil the job ran
        if: success()
        run: curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/${{ secrets.VIGIL_PING_TOKEN }}

GitHub runs schedules in UTC, can delay them at busy times, and disables scheduled workflows in public repos after 60 days without activity.

Common mistakes

Server timezone. cron uses the machine's local timezone, and most cloud VMs and containers are set to UTC. Check with timedatectl or date. cronie (RHEL, Fedora, Arch) accepts CRON_TZ=Asia/Kolkata at the top of a crontab; Debian/Ubuntu cron does not.
Daylight saving time. On DST days, local times around 01:00-03:00 are skipped or repeated. Vixie cron/cronie try to run skipped fixed-time jobs just after the jump and not twice in the repeated hour; Kubernetes, systemd and most libraries behave differently. Schedule in UTC if 'exactly once' matters.
Day-of-month OR day-of-week. If both the 3rd and 5th fields are restricted, cron runs when either matches: 0 0 13 * 5 runs on every 13th and every Friday, not only on Friday the 13th.
Unescaped %. In a crontab line, % is turned into a newline (everything after it goes to the command's stdin). Write date +\%F, not date +%F.
A different environment. cron runs with a minimal PATH (often just /usr/bin:/bin), no login shell and no .bashrc. Use absolute paths and set variables at the top of the crontab.
Silent failures. cron doesn't tell anyone when a job fails or stops running; output goes to local mail nobody reads. A heartbeat monitor that expects a ping after every run is the only reliable way to notice.

How to get alerted when it doesn't run

A cron job that stops running looks exactly like one that's working: no errors, no output. The reliable fix is a heartbeat (dead man's switch): the job pings a URL each time it finishes, and the monitor alerts you when a ping is late.

  1. Create a heartbeat in Vigil and choose cron schedule: 0 0 1 * *, timezone of your server.
  2. Set a grace period of about 60 minutes: long enough for a normal run to finish, short enough that you hear about it quickly.
  3. Add the ping to the end of the command (above). && means only successful runs ping; use /ping/<token>/$? to report the exit code, and /start to catch jobs that hang.

Watch this schedule — get alerted if it doesn't run

Related: twice a month (1st and 15th) 0 0 1,15 * * · last day of the month 0 23 28-31 * * · first Monday of the month 0 9 1-7 * * · every quarter 0 0 1 1,4,7,10 * · every 6 months 0 0 1 1,7 * · once a year 0 0 1 1 *
How Vigil can watch this for you

Vigil tells you when a run is missed

Vigil heartbeats understand cron expressions and timezones, so they know exactly when each run is due. Missed, failed or stuck runs alert you on email, Slack, Telegram, Discord or a webhook, and the history shows every run's duration.

Monitor this cron job free
Free plan, no card. Alerts on email, Telegram, Slack, Discord, Teams or any webhook.
More free tools: