Cron every hour is 0 * * * * — a job that runs at minute 0 of every hour, every day, every month, every weekday. The expression is the canonical answer, and the canonical answer is correct. The interesting part is the three other expressions that mean almost the same thing, the way different cron parsers disagree, and the gotchas (DST, time zones, the ”@ hourly” alias) that quietly break a schedule that was supposed to be simple.
The reason “cron every hour” is its own question and not just “how to write cron” is that the question is a favorite test case. Every developer’s first cron job is “run every hour.” Every developer’s first cron bug is “I thought it would run every hour, but it ran at 1:00, 1:05, 1:10, and then never.” The shape of the bug is usually “I used a wrong field, a wrong alias, or a wrong time zone.” The fix is in the field, the alias, or the time zone.
Table of contents
- The short version
- The three expressions that mean “every hour”
- The four expressions that look like every hour but are not
- The five cron aliases that save typing
- The way time zones change what “every hour” means
- The mistakes that quietly break an hourly schedule
- FAQ
The short version
The cron syntax is five time fields: minute, hour, day of month, month, day of week. The wildcard * means “every.” The expression 0 * * * * means “at minute 0 of every hour.” The expression is the canonical answer. The interesting part is the three other expressions that mean the same thing (0 0 * * * * in 6-field extended cron, @hourly in the alias syntax, and 0 */1 * * * in the step syntax), and the four expressions that look like every hour but are not.
The three expressions that mean “every hour”
A short, opinionated list of the three expressions that mean “run at the top of every hour.” The three 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. This is the expression every cron tutorial shows first, and the one that works in every cron parser that has ever existed. The expression is also the one that crontab.guru shows when you search for “every 1 hour.” If the developer is not sure which expression to use, this is the one.
0 */1 * * * — the step syntax. Minute 0, every 1 hour (the */1 step), every day, every month, every weekday. 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 developers who want to make the “every 1 hour” intent explicit, and for cron parsers that prefer the step syntax.
@hourly — the alias syntax. 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 developers who want a cron file that reads like English. The expression is also the right answer for developers who are using a cron parser that has a strict whitelist of allowed characters and rejects the * in some positions.
The three expressions all mean the same thing in a spec-compliant parser. The three expressions may not all mean the same thing in a custom parser, and the developer should test the parser before trusting the expression. The platform’s documentation usually shows the supported form, and the developer should follow it.
The four expressions that look like every hour but are not
A short, opinionated list of expressions that look like they should mean “every hour” but actually mean something different. The four are the source of most “I thought it would run every hour, but it ran twice in the first hour and then never” bugs.
* * * * * — every minute, not every hour. A developer who puts * in the minute field is asking the cron daemon to run the job at every minute of every hour. The job runs 60 times per hour, not once per hour. The fix is to set the minute field to a specific value (0 for the top of the hour, 30 for the half-hour, etc.), not to *.
0 0 * * * — every day at midnight, not every hour. A developer who puts 0 0 in the minute and hour fields is asking the cron daemon to run the job at 00:00 every day. The job runs once per day, not 24 times per day. The fix is to put * in the hour field, not 0.
0 */2 * * * — every 2 hours, not every hour. A developer who puts */2 in the hour field is asking the cron daemon to run the job every 2 hours, not every hour. The job runs 12 times per day, not 24. The fix is */1 for every hour, or just * in the hour field with 0 in the minute field.
0 0-23 * * * — every hour, but the syntax is off. The expression is actually a valid 5-field cron, but it is not the form most parsers expect for “every hour.” The expression means “minute 0 of every hour from 0 to 23, every day.” Mathematically the same as 0 * * * *, but the form is less common. The fix is to use the canonical form unless the parser requires the range.
The four expressions are the floor of the bugs. There are more subtle ones (DST, time zones, the day-of-month/day-of-week OR rule), and the developer should know the cron spec before they trust a non-canonical expression.
The five cron aliases that save typing
The cron spec defines five aliases that read like English. The aliases are supported by Vixie cron, by systemd timers, and by most modern cron parsers. The five are:
@reboot — run once at startup. The right answer for a job that should run when the box comes up, not on a schedule. The job is run as part of the boot process, and the developer does not have to know when the boot happened.
@hourly — run at the top of every hour. The right answer for a job that should run 24 times per day, at the same minute of every hour. The expression is defined as 0 * * * *.
@daily — run at midnight every day. The right answer for a job that should run once per day, at the same time. The expression is defined as 0 0 * * *.
@weekly — run at midnight on Sunday every week. The right answer for a job that should run once per week, on the same day. The expression is defined as 0 0 * * 0.
@monthly — run at midnight on the 1st of every month. The right answer for a job that should run once per month, on the same date. The expression is defined as 0 0 1 * *.
The five aliases are the floor. There is also @yearly (or @annually), which is 0 0 1 1 *. The aliases read better than the expanded form, and they are easier to grep for when something breaks. The developer should use the alias when the schedule is one of the canonical cadences, and should use the expanded form when the schedule is custom.
The way time zones change what “every hour” means
The cron daemon runs in the system’s local time zone, by default. The local time zone is whatever the box is configured to use, and the box’s time zone is usually UTC on a server but is sometimes the operator’s local time zone on a dev machine. The time zone is the part that quietly breaks an “every hour” job that was supposed to be predictable.
A box that is configured in UTC will run 0 * * * * at 00:00, 01:00, 02:00, …, 23:00 UTC. A box that is configured in America/Los_Angeles will run the same expression at 00:00, 01:00, 02:00, …, 23:00 PST (or PDT during daylight saving). The two are not the same. A job that is supposed to run “at the top of every hour, in the user’s local time” is a job that needs the time zone to be set explicitly, not inherited from the system.
The CRON_TZ environment variable. Some cron parsers (Vixie cron, dcron) support a CRON_TZ=<timezone> environment variable that sets the time zone for the crontab. The variable is set at the top of the crontab, and every job in the crontab runs in the specified time zone. The variable is the right answer for a job that needs to run at a specific local time, regardless of the system’s time zone.
The system’s time zone. The default behavior. The job runs in the system’s time zone, which is whatever /etc/timezone says (Debian-flavored systems) or whatever /etc/localtime is a symlink to (RHEL-flavored systems). The developer should know what the box’s time zone is, and should not assume it is UTC.
Daylight saving. A job that runs 0 * * * * on the day of a DST jump will run 23 or 25 times that day, depending on the direction of the jump. The job is not technically broken — the cron daemon is doing what it was told — but the job is going to surprise the developer. The fix is to move the job outside the DST window (e.g. 0 1-23 for the second hour of every day), or to set the time zone to UTC and convert at the application layer.
The mistakes that quietly break an hourly schedule
A short, opinionated list of mistakes that have actually broken real hourly schedules. None of them are dramatic. They are the boring ones.
The * in the minute field. A developer who writes * * * * * is asking for every minute, not every hour. The fix is to set the minute field to 0, not to *. The bug is the single most common cron bug in real codebases.
The 6-field cron in a 5-field parser. Some cron parsers (Quartz, Spring) support 6 fields, with the seconds field first. A developer who writes 0 * * * * * in a 5-field parser is asking for 0 * * * * (the parser ignores the 6th field), which is fine, but a developer who writes * * * * * in a 6-field parser is asking for every second. The fix is to know which parser the platform uses.
The platform’s time zone is not the developer’s time zone. A job that is supposed to run “at the top of every hour, in the user’s time zone” is a job that needs CRON_TZ=America/Los_Angeles (or whatever the user’s time zone is) at the top of the crontab. The default behavior is the system’s time zone, which is usually UTC on a server, which is not the user’s time zone.
The job is set to a 6-field expression on a 5-field platform. A platform that uses a strict 5-field parser will reject a 6-field expression, or worse, will silently ignore the seconds field. The fix is to know the platform’s parser, and to use the form the platform supports.
The DST transition is happening. A job that runs on the day of a DST jump will run 23 or 25 times that day. The fix is to set the time zone to UTC, or to move the job outside the DST window, or to add a check in the job that skips the extra run.
The job is not idempotent. An “every hour” job that runs twice in the same hour (because of a DST jump, because of a manual trigger, because of a deploy) is a job that will produce duplicate data. The fix is to make the job idempotent — the job should check whether the work has already been done before doing it, or should use a unique key that prevents duplicates.
How this fits the rest of the stack
An hourly job rarely lives in isolation. The job usually calls an API, queries a database, sends an email, or writes a file to storage. 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 console 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 cron expression for every hour?
The canonical expression is 0 * * * * (minute 0, every hour, every day, every month, every weekday). The step syntax 0 */1 * * * and the alias @hourly are equivalent in spec-compliant parsers. The expression runs at the top of every hour, in the system’s local time zone.
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 cron bug is using * in the minute field when the developer meant a specific minute.
What is @hourly in cron?
@hourly is an alias that means “run at the top of every hour.” The expression is defined as 0 * * * * in the cron spec, and is supported by Vixie cron, systemd timers, and most modern cron parsers. The alias is the right answer for developers who want a cron file that reads like English.
How do I run a cron job every hour on the half hour?
Use 30 * * * * (minute 30, every hour). The expression runs at 00:30, 01:30, 02:30, …, 23:30. 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 *.
How do I run a cron job every hour starting at a specific time?
Use 0 3-23 * * * for “every hour starting at 3 a.m.,” or use the offset syntax if the parser supports it. The pattern is the right answer for a job that needs to run on a specific cadence (every hour, but not on the top of the hour). The first run will be at 3:00, the next at 4:00, and so on.
How does the cron time zone work?
By default, cron runs in the system’s local time zone. The system time zone is whatever the box is configured to use, and the developer can set it with CRON_TZ=<timezone> at the top of the crontab. A job that is supposed to run “at the top of every hour, in the user’s time zone” is a job that needs the time zone to be set explicitly, not inherited from the system.
Can I run a cron job every hour on a managed platform?
Yes. Most managed platforms (RunxBuild, Render, Fly, Heroku, Vercel) have a cron-job or scheduled-task feature that wraps the cron syntax in a UI. The developer sets the schedule in the dashboard, and the platform runs the job on the schedule. The platform handles the time zone, the logging, the retries, and the failure alerts — the developer just writes the job and ships it.