Crontab Every Hour: The Expression, the Gotchas, and the Three Patterns

Sean

Platform Writer

Jun 10, 2026
5 min read

A crontab entry that runs every hour is the line 0 * * * * /path/to/command — a job that runs at minute 0 of every hour, every day, every month, every weekday. The crontab is the file that lists the scheduled jobs, the cron daemon is the process that reads the crontab and runs the jobs, and the entry is the line that tells the daemon when to run the command. The expression is the heart of the entry, and the expression is the part the developer has to get right to ship a job that runs predictably.

The reason “crontab run every hour” is its own question and not just “how to use crontab” is that the question is the most common real-world crontab entry, and the question is the one that surfaces every crontab gotcha. A crontab entry that is supposed to run every hour is a crontab entry that the developer is going to debug the first time it does not run. The fix is in the expression, the time zone, the PATH, or the command.

Crontab every hour: the expression, the gotchas, and the three patterns

Table of contents

The short version

The crontab is a per-user file (at /var/spool/cron/crontabs/<user> on most systems) that lists the scheduled jobs. The cron daemon reads the crontab every minute, evaluates every entry, and runs the jobs whose time expressions match the current minute. The every-hour expression is 0 * * * * — minute 0, every hour, every day, every month, every weekday. The cron daemon runs the command, captures stdout and stderr, and (by default) emails the user with the output.

The five fields the crontab line has

A crontab line has five time fields, followed by a command. The fields are evaluated by the cron daemon every minute, and the job runs if every field matches the current minute. The five fields are:

Minute (0–59). The minute of the hour. The value is a specific minute (0, 15, 30, 45), a wildcard (* = every minute), a list (0,15,30,45), a range (0-15), or a step (*/15 = every 15 minutes, 0-59/15 = every 15 minutes starting at 0).

Hour (0–23). The hour of the day. The format is the same as the minute. The value 0 is midnight, 12 is noon, 23 is 11 p.m. The value */2 is every 2 hours.

Day of month (1–31). The day of the month. The format is the same. The value * is every day. The value 1 is the 1st of the month.

Month (1–12, or named). The month of the year. The value can be a number (1 for January) or a name (jan, feb, etc.). The value * is every month.

Day of week (0–7, or named). The day of the week. The value 0 and 7 are both Sunday, 1 is Monday, 6 is Saturday. The value can be a number or a name (sun, mon, etc.). The value * is every weekday.

The OR rule. When both the day-of-month and the day-of-week are restricted (not *), the job runs when EITHER matches, not both. This is the OR rule, and it is the source of the “I thought it would run on Fridays, but it also ran on the 1st” bug. The fix is to use a specific day-of-month or day-of-week, not both.

# m  h  dom  mon  dow  command
  0  *  *    *    *    /usr/local/bin/backup.sh

The four variations on the every-hour expression

The every-hour expression is 0 * * * *, but there are four variations a developer will see. The four are not interchangeable across all cron parsers, and the developer should know which one their parser accepts.

0 * * * * — the canonical answer. Minute 0, every hour, every day, every month, every weekday. The expression is the one every crontab tutorial shows first, and the one that works in every cron parser. The expression runs at the top of every hour, in the system’s local time zone.

0 */1 * * * — the explicit step. Minute 0, every 1 hour (the */1 step). The expression is mathematically the same as 0 * * * *, because */1 is the same as * in the step syntax. The expression is the right answer for a developer who wants to make the “every 1 hour” intent explicit.

@hourly — the alias. The @hourly alias is defined as 0 * * * * in the cron spec, and is supported by Vixie cron, by systemd timers, and by most modern cron parsers. The expression is the right answer for a developer who wants a crontab that reads like English.

0 0-23 * * * — the range. Minute 0, every hour from 0 to 23. The expression is mathematically the same as 0 * * * *, but the form is less common. The form is the right answer for a developer who wants to exclude a specific hour (0 1-23 for every hour except midnight).

The four variations are the floor. There are also parser-specific extensions (Quartz’s 6-field cron, Spring’s 6-field cron, AWS EventBridge’s cron) that support seconds or years, but the four are the ones the developer should know for the standard 5-field crontab.

The six gotchas that quietly break an hourly crontab entry

A short, opinionated list of gotchas that have actually broken real hourly crontab entries. None of them are dramatic. They are the boring ones.

The * in the minute field. A crontab line that reads * * * * * is asking the cron daemon to run the job every minute, not every hour. The job runs 60 times per hour, not once. The fix is to set the minute field to a specific value (0 for the top of the hour), not to *.

The crontab entry is not on a newline. A developer who edits a crontab with crontab -e and the last line of the file does not end with a newline is a developer whose crontab is not being parsed correctly. The fix is to make sure the last line ends with a newline, and the fix is the lever that turns a “my crontab is broken” bug into a “my crontab is fine” reality.

The PATH does not include the command’s directory. A crontab entry that calls python instead of /usr/bin/python3 is an entry that is going to fail when the system has both Python 2 and Python 3, or no python symlink. The fix is to set PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab, and to call the script with an absolute path.

The output goes nowhere. A crontab entry that produces output but does not redirect it is an entry that emails the user with the output. If the local mail system is not configured, the email sits in the local spool and the developer never sees it. The fix is to redirect stdout and stderr to a log file, and to ship the log file to a log service the developer actually reads.

The time zone is not what the developer thinks. A crontab entry that is supposed to run at the top of every hour in the user’s local time is an entry that is going to run in the system’s time zone, which is usually UTC on a server. The fix is to set CRON_TZ=America/Los_Angeles (or whatever the user’s time zone is) at the top of the crontab, and the fix is the lever that makes the schedule predictable.

DST transitions. A crontab entry that runs at 2:30 a.m. on the day of a DST jump either runs twice or not at all, depending on the direction. For most workloads the answer is to move the job outside the DST window, or to set the time zone to UTC and convert at the application layer.

The way PATH and the environment shape the command

A crontab job runs with a near-empty environment. The PATH is usually /usr/bin:/bin, the TERM is not set, the locale is not set, and the user’s ~/.bashrc is not sourced. A script that works in the developer’s terminal can fail in cron because /usr/local/bin is not on the PATH, or because the script relies on ~/.bashrc. The fix is in the crontab’s environment, and the fix is the lever that makes the job portable.

Set PATH at the top of the crontab. The line PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin at the top of the crontab gives the cron daemon the same PATH the developer has in their terminal. The line is the right answer for any crontab that calls a script in /usr/local/bin (the most common place for custom scripts).

Set the locale at the top of the crontab. The lines LANG=en_US.UTF-8 and LC_ALL=en_US.UTF-8 at the top of the crontab give the cron daemon the same locale the developer has in their terminal. The lines are the right answer for any crontab that runs a script that depends on the locale (a Python script, a Node script, a shell script that calls date).

Set the time zone at the top of the crontab. The line CRON_TZ=America/Los_Angeles at the top of the crontab gives the cron daemon a specific time zone. The line is the right answer for any crontab that needs to run at a specific local time, and the line is the lever that makes the schedule predictable.

Call the script with an absolute path. The line 0 * * * * /usr/local/bin/backup.sh calls the script with an absolute path, regardless of the crontab’s working directory. The path is the right answer for any crontab that calls a script, and the path is the lever that makes the call portable.

Source the user’s ~/.bashrc if needed. The line 0 * * * * /bin/bash -c "source ~/.bashrc && /usr/local/bin/backup.sh" sources the user’s ~/.bashrc before running the script. The line is the right answer for any crontab that needs the user’s environment, and the line is the lever that makes the call portable.

The five settings are the floor. There are also MAILTO= (to disable the default email behavior), SHELL= (to use a specific shell), and HOME= (to use a specific home directory). The five are the ones the developer should know first.

The three patterns a team should standardize on

A short, opinionated list of patterns a team should standardize on. The patterns are the ones that make an hourly crontab entry predictable, debuggable, and portable across machines.

The “PATH + absolute path + log file” pattern. The team standardizes on a crontab entry that sets the PATH, calls the script with an absolute path, and redirects stdout and stderr to a log file. The pattern is the right answer for any team that has multiple developers editing the crontab, and the pattern is the right answer for any team that wants to debug a crontab that did not run.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CRON_TZ=UTC

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

The “wrapper script” pattern. The team standardizes on a small wrapper script (/usr/local/bin/run-job.sh) that sets the environment, sources the user’s profile, runs the actual job, captures the exit code, and emails or logs the result. The pattern is the right answer for any team that has a complex job (one that needs a virtual environment, one that needs to source a config file), and the pattern is the lever that turns a 5-line crontab entry into a 1-line crontab entry.

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

The “managed scheduler” pattern. The team standardizes on a managed scheduler (RunxBuild’s cron jobs, Render’s cron jobs, Heroku Scheduler, AWS EventBridge, Google Cloud Scheduler) instead of crontab on a server. The pattern is the right answer for any team that has more than a handful of jobs, and the pattern is the lever that turns a crontab file on a server into a UI on a platform. The platform handles the time zone, the logging, the retries, the failure alerts, and the deploy history.

The three patterns are not exhaustive. There are also patterns with systemd timers, with Kubernetes CronJobs, and with serverless functions on a schedule. The three are the ones the developer should learn first, and the three are the ones a team should pick one of and stick with.

How this fits the rest of the stack

An hourly job rarely lives in isolation. The job usually calls an API, queries a database, writes a file, or sends a notification. The platform that handles the job should make the rest of the stack feel like part of the same conversation.

The services layer is the part of the platform that runs the long-lived API the hourly job calls. The database layer is the part that holds the data the job reads and writes. The static layer is the part that hosts the dashboard where the job’s status is reported. The environment variables are the part that holds the secrets the job reads at runtime.

An hourly job on a platform where the API, the database, the storage, and the secrets are all in the same place is a job the team is going to be able to debug. An hourly job on a platform where each piece is in a different console is a job the team is going to spend the first hour just opening the right tab.

For a team that wants to see the full cost of the project before it commits, the RunxBuild hosting calculator shows the line items together. The API, the database, the storage, the worker, the bandwidth — each one is a separate number, and the team’s mental model for the platform is the sum of those numbers.

FAQ

What is the crontab line for every hour?

The canonical line is 0 * * * * /path/to/command. The expression 0 * * * * means “at minute 0 of every hour, every day, every month, every weekday.” The command is the absolute path to the script or binary the cron daemon should run. The line is the right answer for any developer who wants a job that runs at the top of every hour.

What is the difference between 0 * * * * and * * * * *?

0 * * * * runs at minute 0 of every hour (24 times per day). * * * * * runs at every minute of every hour (1440 times per day). The difference is the minute field — 0 is a specific minute, * is “every minute.” The most common crontab bug is using * in the minute field when the developer meant a specific minute.

How do I edit a crontab?

Use crontab -e. The command opens the user’s crontab in the default editor ($EDITOR, usually vim or nano), and the command saves the changes to the crontab file. The cron daemon picks up the new crontab automatically — there is no need to restart the daemon. The developer should never edit the crontab file directly (at /var/spool/cron/crontabs/<user>), because the file is managed by the crontab command.

How do I list my crontab entries?

Use crontab -l. The command prints the current crontab to stdout. The command is the right answer for a developer who wants to see what jobs are scheduled, and the command is the right answer for a developer who wants to back up the crontab.

How do I run a crontab entry as a different user?

Use sudo crontab -u <user> -e to edit the user’s crontab as root. The command opens the user’s crontab in the default editor, and the cron daemon runs the job as that user. The pattern is the right answer for a developer who needs to set up a system job that runs as a specific user.

Why is my crontab entry not running?

The most common causes are: the entry is malformed (check the five fields), the command is not on the PATH (use an absolute path), the script is not executable (chmod +x), the cron daemon is not running (systemctl status cron), the output is being silently dropped (redirect to a log file), the time zone is wrong (set CRON_TZ=), or the entry is in the wrong crontab (crontab -u <user> -l to check).

Can a crontab entry run every hour on the half hour?

Yes. Use 30 * * * * to run at minute 30 of every hour (00:30, 01:30, 02:30, etc.). The pattern is the same for any minute of the hour — the developer sets the minute field to the specific minute, and leaves the rest as *.