Cron Is a Pattern Matcher
Five fields, one comparison per minute, and no memory of the minute before. Almost every cron incident is a number you calculated from the expression rather than from the calendar the expression runs against
Published: 2026-10-17 | jslet Research | 17 min read
๐ In This Briefing
Every operator has a cron story, and the stories cluster. A job that was supposed to run monthly ran seven times a year. A job that was supposed to run once a week ran sixty-one times. A five-minute job was supposed to fire 288 times a day and the process table showed two copies of it, then four. In each case the expression was typed correctly and the arithmetic done afterwards was wrong, because the arithmetic assumed cron was something it is not.
Cron is not a scheduler. It is a pattern matcher. Once a minute, the daemon wakes, reads every stored crontab, and asks one question per line: does the current wall-clock minute match this pattern. If yes, fork a shell and move on. Debian's manual describes the loop in a single sentence that is worth memorising, because it is the whole architecture: cron then wakes up every minute, examining all stored crontabs, checking each command to see if it should be run in the current minute.
There is no queue. There is no notion of an interval, no record of whether the last run finished, no list of runs that were supposed to happen. There is a predicate, a clock, and a fork. Everything that surprises people about cron follows from that sentence, and the surprises arrive in four flavours: a step is not an interval, two day fields are combined with OR, a month is not a unit, and nothing is remembered.
This is the written companion to the Cron Trigger Frequency Matrix and the Cron Expression Tester. What follows is the arithmetic those tools exist to save you from: what a step value actually selects, what the day fields actually combine, which month you are dividing by, and what happens to the count when the clock jumps.
Five Fields, One Predicate
The POSIX specification of crontab is more austere than most people expect. It defines six fields per line in the user crontab — five time patterns followed by the command — and it defines the patterns narrowly: each of these patterns can be either an asterisk (meaning all valid values), an element, or a list of elements separated by comma characters. An element shall be either a number or two numbers separated by a hyphen-minus (meaning an inclusive range). That is the entire grammar. An asterisk, a number, a range, a list. Nothing else.
Notice what is missing: the slash. Step values such as */5 are not in the POSIX grammar at all. They are a Vixie cron extension that every Linux distribution ships and almost every developer believes is part of the standard. It is a small point with a practical consequence, which is that the semantics of */N are defined by an implementation manual rather than by a standard, and they are not the semantics most people assume.
The unit of the predicate is one minute. A day has 1,440 of them. A 365-day year has 525,600. The annual-average month, the one twelve of which make a year, has 43,800 — the same 730 hours that every cloud invoice divides by, multiplied by sixty. These three numbers are the entire substrate. Every count in this article is a count of minutes that satisfy a predicate over one of those windows, and the first source of error is picking the wrong window.
Here is the smallest possible demonstration. Take the three most common expressions and count them three ways: per day, per month on the annual-average basis, and per year.
| Expression | Per day | Per month (43,800 min) | Per year (525,600 min) | Per month (30-day) |
|---|---|---|---|---|
* * * * * | 1,440 | 43,800 | 525,600 | 43,200 |
*/5 * * * * | 288 | 8,760 | 105,120 | 8,640 |
0 * * * * | 24 | 730 | 8,760 | 720 |
0 0 * * * | 1 | 30.42 | 365 | 30 |
The hourly job is the cleanest illustration of the whole problem. On the annual-average month it fires exactly 730 times, because a month is 730 hours. On a thirty-day month it fires 720 times. Twelve months of 720 is 8,640; twelve months of 730 is 8,760; and the annual count, 8,760, agrees with the second and not the first. The 8,760 figure looks like a coincidence with the number of hours in a year. It is not a coincidence. It is that number, because an hourly predicate fires once per hour.
None of this is exotic. It is the same denominator problem that makes a monthly cloud bill disagree with a usage report, arriving through a different door. The expression is a predicate. The predicate has no idea what a month is.
*/N Is a Residue Class
The Vixie manual is unusually explicit about what a step does, and the explanation is buried in a note that most readers skip. After describing ranges and steps it warns: skip values only operate within the time period they are attached to. For example, specifying */35 for the minute field of a crontab entry won't cause that entry to be executed every 35 minutes; instead, it will be executed twice every hour, at 0 and 35 minutes past. The manual then offers a shell workaround involving modulo arithmetic against the epoch, which is a fair signal that the feature does not do what people want.
The mechanism is simple once stated. A step applied to an asterisk means "first value to last value, skipping by N", and the field's last value is 59 for minutes and 23 for hours. So */N in the minute field selects every minute whose number is a multiple of N between 0 and 59 inclusive. When N divides 60 exactly, the last selected minute is 60 minus N, and the pattern tiles the hour evenly. When N does not divide 60, the tile does not close: the gap between the last fire of one hour and the first fire of the next is shorter than N, so the schedule fires more often than the nominal interval.
The count per hour is floor(59 / N) + 1. That single expression explains every row in the table below, and the right-hand column is what actually lands on your quota.
| Expression | Minutes selected in each hour | Fires/hour | Nominal 60/N | Error | Fires/year |
|---|---|---|---|---|---|
*/2 | 0, 2, ... 58 | 30 | 30.00 | 0% | 262,800 |
*/5 | 0, 5, ... 55 | 12 | 12.00 | 0% | 105,120 |
*/7 | 0, 7, 14, 21, 28, 35, 42, 49, 56 | 9 | 8.57 | +5.0% | 78,840 |
*/11 | 0, 11, 22, 33, 44, 55 | 6 | 5.45 | +10.0% | 52,560 |
*/13 | 0, 13, 26, 39, 52 | 5 | 4.62 | +8.3% | 43,800 |
*/25 | 0, 25, 50 | 3 | 2.40 | +25.0% | 26,280 |
*/40 | 0, 40 | 2 | 1.50 | +33.3% | 17,520 |
*/59 | 0, 59 | 2 | 1.02 | +96.7% | 17,520 |
The last row is the punchline. */59 reads like a job that runs roughly once an hour. It runs twice an hour, at minute zero and minute fifty-nine, nine seconds of wall clock apart in real time and then fifty-nine minutes of silence. And */59 produces exactly the same number of executions as */30, because both select two minutes out of sixty. The count is identical, the timing is not, and any capacity model that treats the expression as an interval gets the answer wrong by a factor of two.
The collapse is broader than one pair. Every value of N from 30 through 59 yields exactly two fires an hour, because none of them fits twice into fifty-nine. Thirty different spellings, one behaviour. Values 20 through 29 all yield three. Values 15 through 19 yield four. The map from N to load is a step function with a coarse staircase, so a change from */31 to */59 — a step that reads like a big relaxation — changes nothing at all, while a change from */15 to */17 changes nothing either, and a change from */14 to */15 cuts the load by a fifth.
There is one more layer, and the manual states it plainly: step values are evaluated just within the field they are applied to. So */23 in the hour field means hour 0 and hour 23 of each calendar day, not every twenty-three hours. The two fires are twenty-three hours apart once and one hour apart once, every day. Any schedule that genuinely wants a period longer than one field unit needs a different mechanism, which is why the manual's own workaround reaches for the epoch counter.
The Two Day Fields Are OR
This is the single most expensive misunderstanding in the syntax, and both standards state the rule in almost the same words. POSIX: if either the month or day of month is specified as an element or list, and the day of week is also specified as an element or list, then any day matching either the month and day of month, or the day of week, shall be matched. The Vixie manual puts it in a note: if both fields are restricted, that is, do not contain the asterisk character, the command will be run when either field matches the current time, and then gives its own example, 30 4 1,15 * 5, which runs at 4:30 on the first and fifteenth of each month plus every Friday.
Read that example slowly. A person writing 1,15 and 5 is almost always trying to say "the Fridays that land on the first or the fifteenth". What they have said is "the first, the fifteenth, and every Friday". The fields are not a conjunction. They are two independent membership tests whose results are ORed.
Now price the mistake at its worst. Suppose the intent is a job that runs on Friday the thirteenth — a bad-luck deploy freeze, a compliance check, a batch job that wants an unlucky date. The natural expression is 0 0 13 * 5. In 2026 the calendar contains 52 Fridays and 12 thirteenths, and three dates are both, so the union is 52 plus 12 minus 3, which is 61. Sixty-one executions where three were intended: a factor of twenty.
| Expression | What it reads like | Intended | Actual 2026 | Ratio |
|---|---|---|---|---|
0 0 13 * 5 | Friday the thirteenth | 3 | 61 | 20.3× |
0 0 1 * 1 | The first that is a Monday | 1 | 63 | 63× |
0 0 31 * * | Monthly, on the last day | 12 | 7 | 0.58× |
0 0 30 * * | Monthly, on the last day | 12 | 11 | 0.92× |
0 0 29 2 * | Leap day | 1 per 4 years | 7 per 28 years | 0.88× |
The second row is the same defect with a different mask: 1 and 1 is the first of the month OR Monday, and in 2026 that is twelve firsts plus fifty-two Mondays minus one overlap, sixty-three runs. Both rows over-run. The third and fourth rows under-run, which is the quieter failure: nobody notices a monthly report that fires seven times instead of twelve until an auditor asks for February.
The repair is mechanical and worth internalising as a habit. If you need a conjunction of the two day dimensions, restrict one field and put the other test inside the command. If you need the last day of the month, you cannot express it in five fields at all — 31 is not "last", it is "the thirty-first", and five months do not have one. Both of those are symptoms of the same thing: the day fields are a membership test on a date, and a date is not a rule.
There is a cheap way to see whether a line has this defect without parsing anything. If both day fields are restricted and the result is not obviously a union you want, the expression means something broader than it reads. That test catches most of the incidents, because the failure mode is always the same direction: OR is more permissive than AND, so the job runs more often, and more often is what gets noticed.
The Month Is Not a Unit
There are three defensible answers to "how many minutes in a month", and they differ by up to 6% depending on which month you pick. Calendar months run 28, 29, 30 or 31 days. The billing month that cloud providers use is 730 hours, which is the average month across a year: 365 days times 24 hours divided by 12. The thirty-day month is a finance convention, and it is the one most frequency tables quietly use, because it makes the arithmetic tidy.
Those choices give 43,800 minutes, 43,200 minutes, or anywhere from 40,320 to 44,640 if you name a specific month. For an every-minute job the annual count is 525,600 and the annual-average month is 43,800. If a table tells you 43,200, it is not wrong in the way that a typo is wrong — it is answering a different question, and the difference is 600 executions a month, 7,200 a year, or 1.4%.
That 1.4% is the same 1.4% that separates a 720-hour month from a 730-hour one, and it accumulates in exactly the same place: the annual reconciliation. A capacity model built on 43,200 overstates available headroom by 600 executions a month, and the first month it gets audited is the month the quota alarm fires at 99% rather than 97%.
The worse version of this problem is structural rather than numerical. Cron has no month concept at all, so a job that is supposed to happen once a month has to be expressed as a date match, and dates are not uniformly distributed:
| Expression | Fires/year | What is actually happening |
|---|---|---|
0 0 1 * * | 12 | The only day number that exists in every month |
0 0 15 * * | 12 | Also universal, and the usual compromise |
0 0 30 * * | 11 | February has no thirtieth; silently skipped |
0 0 31 * * | 7 | Only January, March, May, July, August, October, December |
0 0 29 2 * | 0 in 2026 | Seven firings across the twenty-eight years to 2052 |
0 0 * * 0 | 52 | Weekly, and 2026 happens to start on a Thursday |
Vixie cron offers no last-day token, no weekday-of-month construct, and no year field. The five-field grammar cannot say "the last Friday" or "the first business day" or "not in December". Every one of those needs the date test moved into the command, where it becomes a line of shell that can fail quietly if the timezone is wrong.
This is also the point where the frequency matrix stops being a convenience and becomes a correctness tool. The number you want is not derivable from the expression by inspection, because the expression does not contain the calendar. It has to be counted against a calendar, which is a different operation, and it is the operation the frequency calculator exists to do.
One useful sanity rule falls out of the table. A daily job's monthly average is 30.42, not 30. A weekly job's monthly average is 4.35, not 4. A monthly job is 12 a year, not 12 in every month. As soon as a capacity or quota document rounds those to 30, 4 and 12, it is already 1.4% optimistic before anyone has typed a step value.
Frequency Is Not Load
Every count so far has treated executions as independent events spread evenly through time. They are not. Cron has minute resolution, which means every job matching the same pattern fires in the same second, and the second is the first second of the matching minute. Two hundred services running */5 * * * * do not produce two hundred executions distributed across five minutes. They produce two hundred simultaneous executions, 288 times a day.
The arithmetic of that is worth writing down because it is where the capacity model usually breaks. Two hundred services times 288 fires a day is 57,600 executions a day, which is 1.75 million a month. Spread evenly, that is about 0.67 executions a second. Concentrated at the minute boundary, it is a burst of 200 concurrent invocations, 288 times a day, with near-zero traffic in between. The average and the peak differ by a factor of three hundred, and the thing that falls over is the peak.
FreeBSD's cron grew an option specifically for this. The manual for -j jitter says the daemon will sleep a random number of seconds in the range from 0 to jitter before executing, and that the option can help to smooth down system load spikes during moments when a lot of jobs are likely to start at once, e.g., at the beginning of the first minute of each hour. That sentence is a description of the default behaviour of every cron on Linux, which has no jitter at all.
The second half of the load problem is overlap, and it is worse than the burst because it compounds. Cron forks a shell and does not track the child. If the job takes longer than the interval, the next fire starts a second copy while the first is still running, and the third fire starts a third. The process count grows until the job's duration stops growing, which it does only when the job starts contending for its own resources.
| Schedule | Job runtime | Steady-state copies | What the operator sees |
|---|---|---|---|
*/5 * * * * | 3 min | 1 | Nothing unusual |
*/5 * * * * | 7 min | 2 | Double the memory, half the throughput per copy |
*/5 * * * * | 20 min | 4 | Lock contention, then timeouts, then more retries |
* * * * * | 3 min | 3 | A queue that was never designed as a queue |
The steady-state copy count is the ceiling of runtime divided by interval, and it is stable rather than runaway only because a slower job means fewer completed runs, not more starts. The starts keep coming at the scheduled rate regardless. That is the definition of an open-loop controller, and it is why a job that degrades under load degrades superlinearly: contention makes it slower, slower means more concurrent copies, and more copies means more contention.
The fix is not in the expression, because the expression cannot express it. It is a lock. flock -n around the command turns a second concurrent start into an immediate exit, which converts an unbounded pile-up into a skip. timeout bounds the runtime so the pile-up cannot start. Neither is a cron feature; both are conventions that every operator eventually reinvents, and anacron is the one scheduler in the family that documents locking as a property: its manual notes that active jobs are quoted as locked so that other copies cannot run them at the same time.
There is an important inversion here that most capacity documents miss. For the same monthly volume, a rarer schedule is worse. 730 fires a month at 200 concurrent is 730 bursts of 200. 43,800 fires a month at one concurrent is a steady trickle. The hourly job is the one that takes the database down, and it is the one whose monthly count looks harmless.
Nothing Remembers
The predicate has no memory, and the consequence is that a minute which passes while the daemon cannot run the job is gone. Not deferred, not coalesced, not retried. The daemon was not running, so the question was never asked, so the run never existed. There is no record that it should have.
This is the gap anacron was written to fill, and its manual states the contrast in its second sentence: unlike cron, it does not assume that the machine is running continuously. Anacron stores a timestamp per job and checks whether the job has run in the last n days, which turns "run at 03:00" into "run once per day, whenever the machine is up". That is a different contract, and it is the one most laptops and most home labs actually need.
The same gap exists in the systems that replaced cron, and the modern ones handle it by adding the state cron never had — each with a different rule about how much catch-up is acceptable.
| Runtime | Behaviour after downtime | Catch-up? |
|---|---|---|
| Vixie cron / cronie | The missed minute is gone; the daemon only evaluates the present | No |
| anacron | Runs the job if it has not run in n days, with a configured delay | Yes, by day |
| systemd timer | Persistent=true stores the last trigger and fires immediately on activation; default is false | Only if set |
| Kubernetes CronJob | Catches up, but refuses if more than 100 schedules were missed | Capped |
Kubernetes is the most instructive because it exposes the cap as a number you can reason about. The documentation says the controller checks how many schedules it missed in the duration from its last scheduled time until now. If there are more than 100 missed schedules, then it does not start the Job and logs the error, and the error names the remedy: set or decrease startingDeadlineSeconds or check clock skew. A per-minute CronJob whose controller is down for two hours has missed 120 schedules and will not fire at all when it comes back; the same job with a deadline of 200 seconds only counts the last three and starts normally.
That threshold is a design decision about which failure is worse. Unbounded catch-up on a per-minute job means 10,080 queued executions after a week of downtime, which is a denial of service against your own dependency. Refusing to start means the job quietly does not run, which is a monitoring problem. Kubernetes picked the second and made the number configurable, and the documentation is honest about the residual: the controller creates a Job approximately once per execution time of its schedule, because there are certain circumstances where two Jobs might be created, or no Job might be created, so the work should be idempotent.
systemd's version of the same trade is a single boolean, and its documentation is precise about what it buys: with Persistent=true, the last trigger time is stored on disk, and when the timer is activated the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive. Note the phrasing — at least once, regardless of how many firings were missed. A week of downtime produces one catch-up run, not a week of them. The related behaviour for suspend is documented separately and closes the loop: a calendar timer that elapses during sleep is processed on resume, and if it elapsed more than once, the timer will only result in a single service activation.
The pattern across all four is the same: cron alone cannot be at-least-once, because at-least-once requires knowing what should have happened. Every system that offers the guarantee is storing a timestamp, a counter, or a deadline that cron does not have.
The Clock Is an Input
The predicate is evaluated against local wall-clock time, which means the predicate's input is not a monotonic count of minutes. Twice a year in most of the northern hemisphere the clock is deliberately discontinuous. In 2026 the United States springs forward on Sunday 8 March and falls back on Sunday 1 November. On the first date, local times from 02:00 to 02:59 never occur. On the second, 01:00 to 01:59 occurs twice.
The folklore says the autumn repetition makes a job run twice. That is what a naive wall-clock predicate would do, and it is not what any of the three implementations whose documentation states the behaviour actually does. They disagree with each other in three different ways, and none of them doubles.
| Implementation | Spring forward (02:30 vanishes) | Fall back (01:30 repeats) |
|---|---|---|
| Debian cron / OpenBSD cron | Runs it soon after the change | Does not re-run it |
FreeBSD / macOS cron with -s | Executes exactly once | Executes exactly once |
| Amazon EventBridge Scheduler | Skipped entirely | Runs once, not repeated |
Debian's manual is the most specific, and the qualifiers in it matter as much as the rule itself. If the time has moved forwards, those jobs which would have run in the time that was skipped will be run soon after the change. Conversely, if the time has moved backwards by less than 3 hours, those jobs that fall into the repeated time will not be re-run. It then adds the restriction that turns this from a curiosity into a trap: only jobs that run at a particular time, not specified as @hourly, nor with an asterisk in the hour or minute specifier, are affected. Jobs which are specified with wildcards are run based on the new time immediately.
So a wildcard job and a pinned job behave differently on the same day. 30 2 * * * gets the special handling. */30 * * * * does not, and simply follows the new clock, which is correct because a wildcard has no specific wall-clock instant to preserve. And any clock change over three hours is treated as a correction rather than a seasonal shift: clock changes of more than 3 hours are considered to be corrections to the clock, and the new time is used immediately. A timezone database update, a hypervisor resync, or an NTP correction after a long stall can therefore silently swallow or duplicate a window that the DST rules would have protected.
FreeBSD and macOS take the other fork of the same decision. With -s, jobs that fall in a disappeared or duplicated interval are executed exactly once, they are not skipped nor executed twice. If an interval disappears, the job runs at the same absolute point in time it would have occupied in the old timezone — which for a one-hour jump means the first specified minute of the following hour. That is a different answer to the same question: not "catch up later", but "translate the instant".
EventBridge is the simplest and the most explicit about the asymmetry, because the platform owns the clock: when time shifts forward in the Spring, if a cron expression falls on a non-existent date and time, your schedule invocation is skipped. When time shifts backwards in the Fall, your schedule runs only once and does not repeat its invocation. The spring run is lost, the autumn run is not duplicated, and the documentation says so without hedging.
The practical consequence is that the annual count is conserved but the distribution is not. A daily 02:30 job fires 365 times in a normal year either way — one day contributes zero and another contributes two under the naive model, one day shifts and no day doubles under Debian's. The count survives. What does not survive is the assumption that the job will run after a specific event on a specific day, which is exactly the assumption that reconciliation and month-end jobs are built on.
Four Things Called Cron
The word cron covers four syntaxes that agree on the easy cases and disagree on everything else. This matters for the same reason the residue-class point matters: an expression copied from a blog post into a different platform can parse cleanly, mean something else, and fail only at the boundary conditions nobody tests.
| POSIX crontab | Vixie crontab | Amazon EventBridge | |
|---|---|---|---|
| Fields | 5 | 5 | 6, the sixth being the year |
| Year range | none | none | 1970 to 2199 |
Step syntax */N | not defined | defined, field-local | defined |
| Day of week numbering | 0 to 6, Sunday is 0 | 0 to 7, 0 or 7 is Sunday | 1 to 7, Sunday is 1 |
| Both day fields restricted | OR | OR | one field must be ? |
| Last / weekday / nth tokens | none | none | L, W, # |
| Nicknames | none | @daily, @reboot, six more | none |
The ? row is the most consequential. Vixie resolves two restricted day fields with OR, which is permissive and surprising. EventBridge forbids the ambiguity outright: you can't use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other. The same intent, expressed in the two systems, looks different and behaves differently, and the migration path between them is not a copy and paste.
The weekday numbering row is a smaller trap with a sharper edge. In Vixie, 1 is Monday and 0 or 7 is Sunday. In EventBridge, 1 is Sunday. The number in field position five is not a portable symbol; it is a pointer into a table that each implementation defines for itself. A weekly job migrated from a Linux crontab to a cloud scheduler with the 1 left in place moves from Monday to Sunday, and nothing in either system complains.
Kubernetes sits in a fourth position, because it is not really a cron syntax problem at all. Its schedule field accepts cron format, but the interesting behaviour is in the fields around it: timeZone, stable since v1.27, which replaces the CRON_TZ prefix that was never officially supported; concurrencyPolicy with Allow, Forbid and Replace, which is the lock that bare cron does not have; and the 100-missed-schedule cap. Without timeZone, the controller interprets schedules in its own local timezone, which is a server property, not an application property.
The practical conclusion is unglamorous. A cron expression is a portable-looking string with four non-portable semantics inside it: whether a step is legal, what the day fields mean together, what the number five means, and what happens when the clock moves. If an expression matters, name the implementation in the comment above it.
Count the Set
Every error in this article comes from the same move: deriving a count from the shape of the expression instead of counting the minutes that satisfy it. The shape is a summary, and a summary of a predicate is exactly the thing that loses the OR, the residue class and the calendar. The reliable method is dull and cheap: enumerate the minutes.
For a year that is 525,600 iterations, which is nothing. Walk the window a minute at a time, evaluate the five fields, and increment a counter when the whole predicate is true. The evaluator is about twenty lines, and the only part that requires care is the day logic, because that is where the OR lives:
- If both day fields are wildcards, the day matches.
- If only one is restricted, test that one.
- If both are restricted, match when either test passes. Never require both.
- Convert the calendar weekday to the implementation's numbering before comparing, because Sunday is 0, 7, or 1 depending on where you are.
The output of that loop is the only number worth putting in a capacity document, because it is the only one that contains the calendar. Three formulas replace the intuitions that fail, and they are worth keeping next to the evaluator:
| Question | Wrong intuition | Correct count |
|---|---|---|
Fires per hour for */N | 60 / N | floor(59 / N) + 1 |
| Fires per year, daily job | 12 × 30 = 360 | 365 |
| Fires per year, both day fields restricted | |A| × |B| | |A| + |B| − |A ∩ B| |
| Fires per month, any job | per-day × 30 | per-day × 365 / 12 |
The third row is the one that repays the effort. The union of two sets is smaller than their product and larger than either, which is why the Friday-the-thirteenth job over-runs by a factor of twenty rather than nine hundred: the two fields overlap on only three dates, so the union is dominated by the fifty-two Fridays. Getting the sign of that error right is the difference between adding capacity and removing it.
If you would rather not write the loop, the frequency matrix builds the day, week, month and year counts from a rate, and the expression tester shows the next firing times so you can see the minute set rather than infer it. What neither tool can do is know your calendar, so the year count that matters is always the one computed against the calendar you actually run.
One last habit makes the difference permanent. Put the computed count in a comment above the crontab line, with the window it was computed over. A line that says # 8,760/yr on 365d is auditable; a line that says # every 5 min is a restatement of the syntax and contains no information about load. The comment is the only place where the predicate's output is written down, and it is the only thing a reviewer can check without running the enumerator again.
Frequently Asked Questions
How many times does */5 * * * * run per month?
8,760 times, if a month means the annual-average month of 43,800 minutes. Twelve fires an hour times 24 hours is 288 a day, and 288 times 365 divided by 12 is exactly 8,760. The 8,640 figure that circulates is the same sum over a flat 30-day month, which is 43,200 minutes rather than 43,800: a 1.4% difference, the same 1.4% that separates a 720-hour month from a 730-hour one. The annual figure, 105,120, does not depend on the choice.
Why did my cron job run on the wrong days?
Because the day-of-month and day-of-week fields combine with OR, not AND, once both are restricted. POSIX specifies that any day matching either the month-and-day-of-month pair or the day of week shall be matched, and the Vixie manual says the same thing in a note. So 0 0 13 * 5 does not mean the Friday that falls on the thirteenth; it means every thirteenth and every Friday. In 2026 that is 12 thirteenths plus 52 Fridays minus the three dates that are both, which is 61 executions where three were intended.
Does */7 * * * * run every seven minutes?
No. A step value selects the minutes whose number is zero modulo the step, inside the field's own range, so */7 selects 0, 7, 14, 21, 28, 35, 42, 49 and 56 and then the hour wraps to zero again. That is nine executions an hour, 216 a day and 78,840 a year, against the 8.57 per hour an actual seven-minute interval would produce. The distortion grows as the step approaches the field width: */59 fires at minute 0 and minute 59, twice an hour, which is exactly what */30 does.
Can cron schedule the last day of the month?
Not in the five-field Vixie syntax, which has no last-day token and no year field. Writing 0 0 31 * * runs the job only in the seven months that have a thirty-first day, so a job intended to be monthly fires seven times a year. Writing 0 0 30 * * fires eleven times. A twenty-ninth-of-February job fires zero times in 2026 and seven times across the twenty-eight years from 2025 to 2052. The usual workaround is a daily job that tests the date, or a scheduler with an explicit last-day construct.
Does a cron job run twice when the clocks go back?
Not on any implementation whose documentation states the behaviour. Debian's cron and OpenBSD's cron run jobs skipped by a forward jump soon after the change and explicitly do not re-run jobs that fall in a repeated hour. FreeBSD and macOS cron with the -s option execute such jobs exactly once, neither skipped nor duplicated. Amazon EventBridge Scheduler skips the spring invocation and runs the autumn one only once. The widely repeated claim that the repeated hour produces two executions is the behaviour of a predicate applied naively to wall-clock minutes, not the documented behaviour of the schedulers people actually run.
Will cron run a job that was missed while the machine was down?
No. Cron holds no record of what should have happened, so a minute that passes while the daemon is stopped or the machine is off is gone. This is the gap anacron exists to fill, and its manual opens by saying that unlike cron it does not assume the machine is running continuously. Kubernetes CronJob has the same gap with a cap: if more than 100 schedules were missed it refuses to start the job and logs an error. systemd timers close it only when Persistent=true is set, which is not the default.
Methodology & Disclosure
Every count in this article is an enumeration, not an estimate. A minute-by-minute scan of the stated window evaluates the five fields against the calendar and increments a counter when the whole predicate is true, with both restricted day fields combined using OR and the weekday converted to Sunday-is-zero before comparison. No figure in the tables was produced by a formula; the formulas appear only where they are offered as replacements for a wrong intuition, and each was checked against the enumeration that produced the tables.
The declared constants are these. The year window is 2026, which is 365 days, 525,600 minutes, 52 Fridays and 12 thirteenths, with three dates that are both. The annual-average month is 365 divided by 12 days, which is 30.4167 days, 730 hours, 43,800 minutes — the same 730-hour convention used across this site for billing months. The flat comparison month is 30 days, 720 hours, 43,200 minutes, and appears only where the difference between the two conventions is the subject. The leap-day count covers 2025 to 2052 inclusive. Daylight-saving dates for 2026 are the second Sunday in March and the first Sunday in November, which fall on 8 March and 1 November.
No vendor price appears anywhere in this article, deliberately. The quantity that matters for cron is executions and concurrency, and expressing it in dollars requires a per-execution price that belongs to a platform rather than to the scheduler. Where the cost of a schedule is discussed, it is expressed as executions, as concurrent copies, or as the size of a burst, all of which are properties of the predicate and remain true when the price list changes.
The frequency calculator this article accompanies models a rate in runs per hour and active hours per day rather than parsing an expression, which is a deliberate simplification and a limitation worth naming: it can express */5 correctly because twelve is the right count for that step, but it cannot express */7, because nine fires an hour is not a rate the input can hold. Its month is now computed on the annual-average basis, so the two documents agree. Where an expression has to be evaluated rather than modelled, use the expression tester and read the firing times, not the count.
References & Further Reading
- The Open Group. POSIX.1-2024, crontab — the field definitions, the grammar of asterisk, element, list and range, the explicit absence of step syntax, and the sentence that fixes the day-field logic: any day matching either the month and day of month, or the day of week, shall be matched. pubs.opengroup.org
- Vixie cron. crontab(5) — the note that two restricted day fields are combined with OR and the
30 4 1,15 * 5example; the note that steps operate only within their own field and that*/35fires at 0 and 35 past each hour; the 0-7 weekday range with 0 or 7 as Sunday; and the eight@nicknames. man7.org/linux/man-pages/man5/crontab.5.html - Debian. cron(8) — the every-minute evaluation loop, and the daylight-saving rules: forward jumps run the skipped jobs soon after the change, backward jumps do not re-run the repeated ones, only jobs pinned to a specific time are affected, and changes over three hours are treated as clock corrections. manpages.debian.org
- OpenBSD. cron(8) — the same forward-run and backward-do-not-rerun rules stated for a BSD implementation, with the same restriction to jobs that specify a particular time. man.openbsd.org
- FreeBSD. cron(8) — the
-soption, under which jobs in a disappeared or duplicated interval are executed exactly once, neither skipped nor executed twice; and the-j jitteroption, added to smooth load spikes when many jobs start at the same minute. man.freebsd.org - Amazon Web Services. EventBridge Scheduler, schedule types — the six-field expression with a year range of 1970 to 2199, the requirement that one day field be
?when the other is*, the 1-7 day-of-week numbering with Sunday as 1, theL,Wand#tokens, and the daylight-saving behaviour: spring invocations skipped, autumn invocations not repeated. docs.aws.amazon.com/scheduler - Kubernetes. CronJob — the 100-missed-schedule cap and the error it logs,
startingDeadlineSecondsand the ten-second controller interval that makes values under ten seconds unreliable, theAllow,ForbidandReplaceconcurrency policies, thetimeZonefield stable since v1.27, and the statement that a Job is created approximately once per execution time because two Jobs might be created or none. kubernetes.io - systemd. systemd.timer(5) —
Persistent=and the stored last-trigger timestamp that produces one catch-up activation regardless of how many firings were missed;AccuracySec=defaulting to one minute and the coalescing it performs; andRandomizedDelaySec=defaulting to zero. freedesktop.org/software/systemd/man - anacron. anacron(8) — the statement that unlike cron it does not assume the machine is running continuously, the per-job timestamp and day-granularity check, and the locking of active jobs. man7.org/linux/man-pages/man8/anacron.8.html
- Related: Cron Trigger Frequency Matrix · Cron Expression Tester · The 730-Hour Month · Unix Time Is Not a Clock · All DevOps Utilities · Methodology
๐ Copyright & Attribution
© 2026 jslet Research. This article is an original work published on jslet (jslet.com). All rights reserved.
Sharing & Reprinting: You may share excerpts (up to 200 words) with a mandatory, do-follow link back to the original article URL. Full reproduction, translation, or adaptation requires prior written permission. Contact: support@jslet.com.
Model Disclaimer: Counts are enumerations over the stated calendar window using the day-field semantics documented for Vixie cron and POSIX. Behaviour on a specific host depends on which cron implementation it runs, its configured timezone, and how that implementation handles clock changes. Verify against your own daemon before setting quotas or alarms.