The 0 0 * * * Expression
The cron expression 0 0 * * * runs a job at minute 0 of hour 0 (midnight) every day. All five fields map to: minute (0), hour (0), day of month (*), month (*), day of week (*). The wildcards mean “any value,” so the job fires whenever minute=0 and hour=0, which happens once every 24 hours at 00:00.
The @daily Shorthand
Most cron implementations support @daily as an alias for 0 0 * * *. It is also equivalent to @midnight, which some implementations recognize as an additional alias. The behavior is identical, running at the start of each calendar day.
@daily # supported on Vixie cron, cronie, fcron
@midnight # recognized by some implementations as equivalent to @daily
0 0 * * * # always works, portable across all cron implementations
If your crontab is managed by a tool (Ansible, Chef, Puppet, Kubernetes CronJob), check whether the tool supports @daily or only standard 5-field expressions. Kubernetes CronJob supports @daily; some older systems do not.
Timezone Pitfalls
The default: system timezone
Cron reads the timezone from the system (/etc/localtime or TZ environment variable). On most cloud VMs, this defaults to UTC. On older on premise servers, it might be a local timezone. Never assume which one you are on. Check with timedatectl or date before relying on midnight behavior.
Setting TZ per crontab
Vixie cron and cronie support a TZ directive at the top of the crontab:
TZ=America/New_York
0 0 * * * /opt/jobs/nightly-report
This makes the cron daemon interpret all times in that entry using the specified timezone, regardless of the system timezone. Not all cron implementations support this. Verify before using it in production.
Best practice: run cron on UTC
Set your servers to UTC and do the timezone math in your application if needed. This eliminates DST surprises entirely and makes log timestamps unambiguous.
Daylight Saving Time and Daily Cron
DST transitions affect cron jobs scheduled at times that fall within the clock change window (typically 02:00 in regions that observe DST). Midnight is outside this window, so a 0 0 * * * job is not directly affected by most DST transitions.
However, if you interpret “midnight” as “midnight in a local timezone” and your server runs on UTC, you need to adjust the hour field twice a year. During US Eastern Standard Time (UTC-5), midnight ET is 0 5 * * * in UTC. During US Eastern Daylight Time (UTC-4), midnight ET is 0 4 * * * in UTC. Hardcoding either value means the job silently fires at the wrong time for half the year.
The clean solution is the TZ directive (if supported) or UTC based servers with application level timezone handling.
Common Use Cases for Daily Midnight Cron
Database backups
Backups typically run when load is lowest. Midnight avoids overlap with business hours traffic, and the backup completes before the workday starts. For large databases, stagger the backup start by a few minutes past midnight to avoid colliding with other nightly jobs.
Report generation
Daily summaries for dashboards, email digests, or analytics pipelines are typically computed after the previous day’s data is fully collected. Running at midnight ensures the previous day is complete before the report runs.
Data cleanup and archival
Purging expired sessions, archiving old records, or rotating logs are low urgency tasks that run well at midnight. If the cleanup affects performance (table locks, index rebuilds), doing it at off peak hours reduces impact on users.
ETL pipelines
Extract transform load jobs that pull data from external sources often have daily update cycles. A midnight cron aligns with the typical “day boundary” used by most data sources and data warehouses.
Staggering Multiple Daily Jobs
If you have several daily jobs, running all of them at 0 0 * * * can cause contention. Space them out:
0 0 * * * /opt/jobs/backup-database
15 0 * * * /opt/jobs/generate-reports
30 0 * * * /opt/jobs/sync-external-data
45 0 * * * /opt/jobs/cleanup-temp-files
Each job starts 15 minutes after the previous one, giving the earlier job time to finish (or at least get past the most resource intensive phase) before the next one starts.
Verifying Execution
To see when 0 0 * * * will next run:
from croniter import croniter
from datetime import datetime
cron = croniter("0 0 * * *", datetime.now())
for _ in range(7):
print(cron.get_next(datetime))
On a Linux system, verify the job ran with:
grep CRON /var/log/syslog | grep "your-job-name"
# or with systemd:
journalctl -u cron --since "yesterday"