To run a cron job every day at midnight, use 0 0 * * *. That means minute 0, hour 0, every day of the month, every month, every day of the week. If you want to check a variation before deploying it, the Cron Expression Generator explains any 5-field expression in plain English and previews the next run times.

The midnight expression, field by field

0 0 * * * | | | | | | | | | +-- day of week (0-7, Sunday is 0 or 7) | | | +---- month (1-12) | | +------ day of month (1-31) | +-------- hour (0-23) +---------- minute (0-59) 

Hours use a 24-hour clock, so midnight is hour 0, not 12 and not 24. A full crontab line looks like this:

0 0 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 

Edit your crontab with crontab -e. The >> ... 2>&1 part appends both stdout and stderr to a log file. Without it, cron tries to email the output to the local user, which usually goes nowhere.

The @daily and @midnight shortcuts

Many cron implementations (Vixie cron and its descendants) accept aliases. @daily and @midnight both mean 0 0 * * *:

@daily /usr/local/bin/backup.sh @midnight /usr/local/bin/backup.sh 

These are not part of POSIX, so don't assume they work everywhere. Schedulers like GitHub Actions expect plain 5-field expressions. Check your platform's docs, or just use 0 0 * * *, which works almost everywhere.

Midnight cron in GitHub Actions, Kubernetes and Node.js

The expression stays 0 0 * * * on every platform that uses 5-field cron. What changes is where you write it and which time zone "midnight" is measured in.

GitHub Actions

name: nightly on: schedule: - cron: '0 0 * * *' workflow_dispatch: jobs: cleanup: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: ./scripts/nightly.sh 

By default, GitHub Actions evaluates schedule in UTC, so 0 0 * * * is midnight UTC: 9:00 AM in Seoul, and 7:00 PM (EST) or 8:00 PM (EDT) the previous evening in New York. For midnight in a UTC+9 zone, shift the hour back 9 hours: 0 15 * * *. Scheduled runs use the workflow file on the default branch and can start several minutes late when GitHub is busy. The workflow_dispatch trigger lets you run the job by hand to test it.

Kubernetes CronJob

apiVersion: batch/v1 kind: CronJob metadata: name: nightly-cleanup spec: schedule: "0 0 * * *" timeZone: "Asia/Seoul" concurrencyPolicy: Forbid jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: cleanup image: busybox:1.36 command: ["sh", "-c", "echo running nightly cleanup"] 

timeZone takes an IANA zone name and is stable since Kubernetes 1.27. Without it, the schedule follows the time zone of the kube-controller-manager, which is usually UTC. Don't put CRON_TZ= or TZ= inside schedule: Kubernetes doesn't support it and newer versions reject it. concurrencyPolicy: Forbid skips a night if the previous run is still going.

Node.js

import cron from 'node-cron'; cron.schedule('0 0 * * *', () => { console.log('nightly job', new Date().toISOString()); }, { timezone: 'America/New_York' }); 

node-cron runs inside your Node.js process, so the job only fires while that process is running. It suits a long-running server, not serverless functions. The timezone option takes an IANA zone name; without it the process's local time zone is used. For work that must survive restarts and deploys, use system cron, a Kubernetes CronJob or your platform's scheduler instead.

Common daily schedules

Only the minute and hour fields change for most daily jobs. Remember that minute comes first, which trips people up.

  • 0 0 * * * — every day at 12:00 AM (midnight)
  • 30 1 * * * — every day at 1:30 AM
  • 0 6 * * * — every day at 6:00 AM
  • 0 12 * * * — every day at noon
  • 0 18 * * * — every day at 6:00 PM
  • 59 23 * * * — every day at 11:59 PM
  • 0 6,18 * * * — twice a day, at 6:00 AM and 6:00 PM
  • 0 */12 * * * — every 12 hours, at midnight and noon
  • 0 9 * * 1-5 — weekdays at 9:00 AM
  • 0 0 * * 1-5 — weekdays at midnight
  • 0 0 * * 0,6 — Saturdays and Sundays at midnight
  • 0 0 1 * * — midnight on the first day of every month

For jobs that run more often than daily, see cron every 5 minutes.

Pitfalls that cause real bugs

* 0 * * * is not midnight

Leaving the minute as * runs the job every minute of hour 0, which is 60 times between 12:00 and 12:59 AM. Always set the minute explicitly: 0 0 * * *.

Cron uses the server's timezone

Standard cron runs in the system timezone of the machine (or the daemon's configured timezone). Many servers and containers are set to UTC, so "midnight" may be 7 or 8 PM in New York. Check with:

date timedatectl 

If you need midnight in a specific zone, either set the server timezone, or use your scheduler's timezone option if it has one. Some cron implementations support a CRON_TZ= line in the crontab, but this is not universal, so confirm it with your cron's man page.

Daylight saving time

In zones that change clocks, a job scheduled during the skipped or repeated hour can be skipped or run twice, depending on the implementation. Midnight is unaffected in zones that switch at 1 or 2 AM, but a 30 2 * * * job commonly hits the spring-forward gap in the US. If a daily job must run exactly once, schedule it outside your region's DST transition hour or run the server in UTC.

Day-of-month and day-of-week are OR, not AND

If you restrict both fields, standard cron runs when either matches. 0 0 1 * 1 runs at midnight on the 1st of the month and also every Monday, not only on Mondays that fall on the 1st. For "first Monday" you need a guard in the script itself.

Percent signs in crontab lines

In a crontab, an unescaped % is treated as a newline. This breaks commands that use date formatting:

# Broken: everything after the first % is sent as stdin 0 0 * * * tar czf /backups/site-$(date +%F).tgz /var/www # Fixed: escape the percent sign 0 0 * * * tar czf /backups/site-$(date +\%F).tgz /var/www 

Everyone picks midnight

Lots of jobs on a shared machine, or lots of scheduled tasks hitting the same API, all fire at 0 0. If exact midnight doesn't matter, offset it: 7 0 * * * or 43 2 * * * spreads the load.

Cron's environment is minimal

Cron jobs run with a stripped-down PATH and no shell profile. Use absolute paths to scripts and binaries, and set variables at the top of the crontab if needed:

PATH=/usr/local/bin:/usr/bin:/bin 0 0 * * * /usr/local/bin/node /srv/app/cleanup.js 

6-field cron: Quartz, Spring and others

Some schedulers use a different format with a leading seconds field. Midnight there looks different:

# Spring @Scheduled (seconds minute hour day month weekday) 0 0 0 * * * # Quartz (day-of-month or day-of-week must be ?) 0 0 0 * * ? 

If you paste a 5-field expression into a 6-field scheduler, everything shifts one position and the job runs at the wrong time or fails to parse. Know which dialect your tool expects. The Cron Expression Generator works with the standard 5-field format.

Test before you trust it

Before deploying, confirm the next few run times in the timezone your server actually uses. For 0 0 * * *, consecutive runs should be 24 hours apart in local clock time, each at 00:00. The exception is a DST-observing zone, where the real elapsed time between two runs can be 23 or 25 hours around the transition dates. If you log timestamps from the job, you can convert raw epoch values with the Unix Timestamp Converter to check what really ran.

FAQ

Is midnight 0 0 or 0 24 in cron?

It's 0 0. The hour field accepts 0 through 23, so 24 is invalid. Midnight at the start of a day is hour 0.

How do I run a cron job at 11:59 PM every day?

Use 59 23 * * *. That runs at the end of each day, one minute before midnight.

Why didn't my midnight cron job run?

The usual causes are a timezone mismatch (the server's midnight isn't yours), a relative path or missing environment variable in the command, an unescaped %, or a syntax error in the crontab. Redirect output to a log file and check grep CRON /var/log/syslog on Debian and Ubuntu, or journalctl -u cron on systems using journald.

What's the difference between @daily and @midnight?

Nothing. In implementations that support them, both are aliases for 0 0 * * *.

What is the cron expression for midnight in GitHub Actions?

0 0 * * * is midnight UTC. For midnight in your own zone, shift the hour by your UTC offset: 0 15 * * * for midnight in Seoul (UTC+9), or 0 5 * * * for midnight EST in New York (UTC-5), which becomes 1:00 AM local time during daylight saving time. Check any expression in the Cron Expression Generator.

Originally published on DevRivet. You can test any cron expression in the browser with the free Cron Expression Generator.