Cron expression for every day at midnight
The cron expression 0 0 * * *
runs at 12:00 AM. The restricted fields are minute 0 and hour 0; the remaining asterisks match anything. That works out to one run per day.
0 0 * * * Field breakdown
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | The top of the hour (:00) |
| Hour | 0 | 00:00 (midnight) |
| Day of month | * | Every day of the month (1–31) |
| 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 * * * — 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": one run per day.
When to use every day at midnight
Midnight is the default home for nightly backups, daily aggregation of the day that just closed, and log rotation at the calendar boundary: the day is complete, traffic is usually at its lowest, and the date arithmetic inside the job stays simple. Two things are worth settling before you commit to it. Whether midnight in the scheduler's timezone is really midnight where your data lives, and whether every other nightly job on the host also fires at 00:00. Moving a few minutes past the hour costs nothing and avoids the pile-up.
Common mistakes with every day at midnight
Midnight looks like a safe hour, and mostly it is, but the day boundary is exactly where date arithmetic goes wrong. A job that runs at 00:00 and processes "today" processes a day that is zero minutes old; it almost always meant yesterday. Compute the target date explicitly instead of deriving it from the run clock. The second problem is congestion: 00:00 is the most contested minute on any shared host, and on a hosted scheduler it is the most contested minute on the platform. Five past costs nothing. The third is timezone — midnight UTC is mid-afternoon or later across much of the world.
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 * * *'
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 * * *") for more reliable timing.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-cron-job
spec:
schedule: "0 0 * * *"
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 * * *"
}
]
} - • 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 * * * /path/to/command
# With logging:
0 0 * * * /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
-
5 0 * * *At 12:05 AM Five past midnight. The same nightly slot, clear of the single most contested minute on any shared host and on every hosted scheduler. -
0 1 * * *At 01:00 AM At 01:00, still overnight but clear of the day boundary. In zones that spring forward at 02:00 this hour always exists, which 02:00 does not. -
0 0 * * 1-5At 12:00 AM, Monday through Friday Nightly on weekdays only. Skips the Saturday and Sunday runs, at the cost of a 72-hour gap that Monday's run has to absorb. -
0 0,12 * * *At 12:00 AM and 12:00 PM Adds a noon run to the nightly one. Halves the worst-case staleness, and gives a midday checkpoint if the overnight run failed.
Frequently asked questions
How do I read the cron expression 0 0 * * *?
Read the five space-separated fields left to right as minute, hour, day of month, month, and day of week. In 0 0 * * * the minute is 0, hour is 0, day of month is *, 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", and a scheduler that checks all five fields once a minute will start the job one run per day.
What timezone does 0 0 * * * 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: at 00:00 UTC it is still 19:00 on the previous date in New York. A daily job that stamps its output with the local date there will label two consecutive runs with the same day unless it takes the date from UTC. 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.
What happens to a midnight cron job during daylight saving time?
On a host whose timezone observes daylight saving, the local clock can skip or repeat an hour. A schedule at 00:00 is safe in most zones, but 02:00 is not: in zones that spring forward at 02:00 the hour never occurs and vixie-cron and cronie run the job once immediately afterwards, while a fall-back repeats 01:00–01:59 and can trigger a duplicate. Running under UTC removes the problem entirely, which is what Vercel Cron always does and what GitHub Actions does unless a workflow opts into a timezone. If the job must respect local time, avoid the 00:00–03:00 window and make the handler idempotent.
Related cron schedules
-
0 9 * * *Every day at 9 AM -
0 12 * * *Every day at noon -
0 0,12 * * *Twice a day -
0 0 * * 1Every Monday -
0 0 * * 5Every Friday -
0 0 * * 1-5Every weekday
Build a custom schedule in the cron expression generator, or browse every epochkit tool.