Cron expression for every month
The cron expression 0 0 1 * *
runs at 12:00 AM, on day 1 of the month. The restricted fields are minute 0, hour 0, and day of month 1; the remaining asterisks match anything. That works out to 12 runs per year.
0 0 1 * * Field breakdown
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | The top of the hour (:00) |
| Hour | 0 | 00:00 (midnight) |
| Day of month | 1 | Day 1 of the month |
| Month | * | Every month (1–12) |
| Day of week | * | Every day of the week (0–6) |
Next runs
- —
- —
- —
- —
- —
Run times are calculated in your browser timezone. The scheduler that executes the job uses its own — always UTC on Vercel Cron, UTC by default on GitHub Actions, the host clock on crontab.
How this schedule works
Cron evaluates the five fields of 0 0 1 * * — minute, hour, day of month, month, day of week — against the clock once a minute and starts the job on any minute where all five match, which cronstrue reads as "At 12:00 AM, on day 1 of the month": 12 runs per year. Day of month is pinned to 1 and the month field selects every month, while day of week stays an asterisk — so the run lands on whatever weekday that date happens to be, and never drifts to a neighbouring day.
When to use every month
The first of the month at midnight is where billing runs, invoice generation, archival of the month just ended, and monthly credential rotation belong: the previous month is complete, so the job processes a closed period rather than a moving one. It is also the standard workaround for the last-day-of-month problem, which plain cron cannot express. Twelve runs a year means the schedule gets very little practice, so the failure path deserves as much attention as the happy path — exercise it against a period boundary before you trust it.
Common mistakes with every month
Monthly schedules are where the day-of-month field's limits show. There is no way to say "the last day" in standard cron, and 0 0 31 * * simply does not fire in a 30-day month — a billing job written that way misses four months a year and reports no error while doing it. Run on the 1st and process the month that just closed. The second mistake is the both-day-fields rule: adding a day-of-week value turns the schedule into an OR, so 0 0 1 * 1 fires on the 1st and on every Monday, which is not a monthly job at all.
Platform snippets
The same expression, ready to paste into the four schedulers that read five-field cron. The notes below each snippet are the ones that follow from this schedule; the cron expression generator lists every caveat for every platform.
GitHub Actions
name: Scheduled workflow
on:
schedule:
- cron: '0 0 1 * *'
workflow_dispatch:
jobs:
scheduled:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run job
run: echo "Running scheduled job" - • Workflows scheduled at the top of the hour may be delayed during high-load periods. Consider offsetting the minute field (e.g. "5 0 1 * *") for more reliable timing.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cron-job
spec:
schedule: "0 0 1 * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: job
image: my-image:latest
command: ["/bin/sh", "-c", "echo Running"]
restartPolicy: OnFailure - • Do NOT use CRON_TZ or TZ inside the schedule string. Kubernetes 1.29+ rejects this at creation with a validation error; earlier versions issue a warning. Existing CronJobs using TZ/CRON_TZ will continue to warn on update. Use the timeZone field instead.
- • Kubernetes supports shorthand macros: @yearly, @monthly, @weekly, @daily, @hourly.
Vercel Cron
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/cron",
"schedule": "0 0 1 * *"
}
]
} - • Hobby plan: invocations occur within the specified hour (e.g. "0 8 * * *" runs between 08:00:00 and 08:59:59).
crontab
# Edit your crontab: crontab -e
0 0 1 * * /path/to/command
# With logging:
0 0 1 * * /path/to/command >> /var/log/cron.log 2>&1 - • Most modern implementations (vixie-cron, cronie) support shorthand macros: @yearly, @monthly, @weekly, @daily, @hourly, @reboot. These are not defined by POSIX — check your platform.
Variations
-
0 0 15 * *At 12:00 AM, on day 15 of the month The 15th. A mid-month run exists in every month, unlike the 29th, 30th and 31st, which is why mid-month billing dates are so common. -
0 9 1 * *At 09:00 AM, on day 1 of the month The 1st at 09:00. A monthly invoice run that fails at midnight has nobody to catch it; one that fails at 09:00 has the whole day. -
0 0 L * *At 12:00 AM, on the last day of the month The last day of the month, in parsers that implement L. Quartz and cron-parser do; vixie-cron, cronie, Kubernetes and GitHub Actions do not. -
0 0 1 */3 *At 12:00 AM, on day 1 of the month, every 3 months The 1st of every third month. The month field takes a step just as the hour field does, and it counts from January.
Frequently asked questions
How do I read the cron expression 0 0 1 * *?
Read the five space-separated fields left to right as minute, hour, day of month, month, and day of week. In 0 0 1 * * the minute is 0, hour is 0, day of month is 1, month is *, day of week is *. Of the operators cron defines, this expression uses only the ones that matter here: an asterisk matches every value in its field. Put together, cronstrue reads the whole thing as "At 12:00 AM, on day 1 of the month", and a scheduler that checks all five fields once a minute will start the job 12 runs per year.
What timezone does 0 0 1 * * run in?
The expression carries no timezone of its own: it names hour 0, and the scheduler decides which hour 0 that is. Read as UTC, the run lands at 00:00 — 19:00 the previous day in New York, 00:00 in London, and 09:00 in Tokyo on a January date, with daylight saving moving the first two later in the year, so the same UTC instant is 20:00 the previous day in New York in July. That offset crosses a calendar day, and this schedule is pinned to the 1st of the month, so the date moves with it: the run starts at 00:00 UTC on the 1st, which is 19:00 on the last day of the previous month in New York. A job that archives "the period that just closed" is therefore running while that period is, locally, still open. Set the zone rather than converting by hand where you can: GitHub Actions defaults to UTC but takes an optional timezone key beside cron, and a Kubernetes CronJob has spec.timeZone, stable since 1.27. Where you cannot, convert deliberately: Vercel Cron is UTC-only, and a Linux crontab follows the host clock unless CRON_TZ overrides it.
How do I run a cron job on the last day of the month?
Standard Unix cron cannot express it, because the day-of-month field takes fixed numbers and 31 simply never matches in a 30-day month. Some parsers — Quartz and cron-parser among them — support the L character, so 0 0 L * * works there but is not portable. Jenkins does not accept L in its built-in timer; that requires the Extended Timer Trigger plugin. The universal workaround is to run daily and exit unless today is the last day: schedule 0 0 * * * and test whether tomorrow's date falls in a different month. Another common trick is to run at 00:00 on the 1st and process the month that just ended.
Related cron schedules
-
0 0 1 */3 *Every quarter -
0 0 1 1 *Every year -
* * * * *Every minute -
*/5 * * * *Every 5 minutes -
*/10 * * * *Every 10 minutes -
*/15 * * * *Every 15 minutes
Build a custom schedule in the cron expression generator, or browse every epochkit tool.