Time and Scheduling: cron Fields, Timezones, Dates
Time arrives in three shapes - an absolute instant, a rule for the future, and a date written into a string. Each fails differently, and all three produce the same two symptoms: hours that are off, or dates that do not exist.
- cron treats day-of-month and day-of-week as OR when both are restricted, so
0 0 1 * 1fires on the 1st and on every Monday - and implementations disagree on this - A timestamp is seconds since 1970-01-01 00:00:00 UTC and carries no timezone of its own; a timezone only enters when it is rendered as a date
- A seconds timestamp stored in a signed 32-bit integer overflows to a negative number after 2038-01-19 03:14:07
- Ten digits means seconds and thirteen means milliseconds, while eleven digits is the genuinely ambiguous case and is read as seconds, roughly the year 2286
- Birth dates inside ID numbers must reject impossible dates such as 30 February, and 15-digit legacy numbers carry no check digit at all
#Three shapes, three failures
Time is hard to debug because it exists in three completely different forms inside a system: a number, a rule, and a string. Their failure modes share nothing.
| Shape | Example | Typical failure |
|---|---|---|
| Absolute instant | 1780000000 | Seconds read as milliseconds; 8-hour offset |
| Future rule | 0 9 * * 1-5 | Day-of-week semantics; timezone mismatch |
| Date string | 19900230 | The date does not exist, yet the format is valid |
#cron: five fields and the meaning of OR
A cron expression has five fields: minute, hour, day of month, month, day of week. When both day fields are restricted, standard Vixie cron treats them as OR. That is the easiest rule to get backwards.
0 0 1 * 1 means "the 1st of the month or every Monday", not "the 1st when it is also a Monday". Reading it as AND would produce a job that fires only a few times a year, which looks exactly like a schedule that never ran.
With the cron generator, several boundaries matter:
- Only digits plus
*(any),/(step),-(range) and,(list) are accepted - Letter forms such as
MON-FRI, Quartz's?LW, and macros such as@dailyare rejected - they are different dialects altogether - The "next run" time is searched in the browser's local timezone, not the server's
Timezone mismatch is the most common reason a schedule drifts. The tool computes in your browser's timezone while the server executes in its own. When they differ, convert by the offset - a 9am job can run at 5pm for you.
Also, an expression like 0 0 29 2 * (leap day) may not trigger within two years, so the tool reports that no next run was found. That is correct behaviour, not a bug.
#Timestamps have no timezone
A Unix timestamp counts seconds from 1970-01-01 00:00:00 UTC and contains no timezone information. The same digits therefore describe the same instant everywhere; a timezone only enters when it is rendered as a date.
The Unix timestamp converter prints local time alongside UTC, Beijing, Tokyo, London and New York. London and New York shift with daylight saving, which is why their offsets read UTC+0/+1 and UTC-5/-4; Beijing observes no DST and stays at UTC+8.
Reverse conversion uses the browser's timezone and there is no timezone selector. To get the seconds for a given UTC moment, fold the offset in yourself: enter 20:00 local for UTC 12:00 at UTC+8, or you will be hours off.
The copy button copies the 13-digit millisecond value. For seconds, drop the last three digits. Reading a 10-digit value as milliseconds is the only reason a date ever shows up as 1970.
#2038 and the eleven-digit ambiguity
Storing a seconds timestamp in a signed 32-bit integer caps it at 2147483647, which is 2038-01-19 03:14:07 UTC; beyond that it overflows to a negative number. This is the year 2038 problem.
Modern runtimes mostly use 64-bit integers or floating seconds, but legacy systems, embedded devices and some database columns still carry the risk. To check, add a far-future offset to a seconds value and see whether it turns negative.
The seconds-versus-milliseconds rule is worth memorising:
- 10 digits: seconds, roughly 2001 to 2286
- 13 digits: milliseconds
- 11 digits: the genuinely ambiguous case, read as seconds, so about the year 2286 - add or drop three digits yourself
An earlier version used a 1e12 threshold, which parsed the year-2000 millisecond value 946684800000 as the year 31969. The threshold is now 1e11 - twelve digits and above means milliseconds - so that misreading is gone.
#Date strings: why a date can be impossible
A value like 19900230 is perfectly well-formed and refers to a day that does not exist. That is the most hidden trap in string dates. Range checks - month 1 to 12, day 1 to 31 - let it through.
The ID card parser handles the birth date by constructing the date from year, month and day and reading it back to compare, which rejects 30 February. It also flags a birth date later than today.
Two more ID-card details get misjudged:
- 15-digit legacy numbers only appear before 1999: no check digit, two-digit year expanded as 19xx. Only the format and plausibility of the birth date can be verified, and the page says so explicitly rather than reporting a pass
- The zodiac year follows the Gregorian year, so anyone born before Lunar New Year belongs to the previous year's sign - birthdays in January and early February need manual checking
An 18-digit ID number has a deterministic check digit: the first 17 digits are weighted, summed, taken modulo 11, and mapped through 10X98765432. A pass means only that the last digit matches - not that the number exists. Anyone can run the same arithmetic to fabricate a valid number; real verification needs police or carrier records.
#Which tool to use
| What you need | Tool | Key constraint |
|---|---|---|
| Convert seconds or milliseconds to a date | Unix timestamp converter | 11 digits is ambiguous; prints five timezones |
| Write a schedule expression | cron generator | Five fields, four syntaxes, day fields are OR |
| Verify a birth date inside an ID number | ID card parser | No check digit on 15-digit numbers |
| Compare two time strings | Text diff | Character-level, so 2026-11-05 differs from 2026-11-5 |
| Generate unique event IDs | UUID generator | v1 embeds a timestamp, v4 is random |
Suggested order: decide what the time actually is - an instant, a rule, or a string - then pick the tool. Treating the three as one is how you end up circling between timezones, leap days and 30 February.
#Frequently asked questions
#Why does my "the 1st and Monday" cron not work?
Because the standard semantics are OR: 0 0 1 * 1 fires on the 1st and also on every Monday. Expressing AND is not possible in standard cron and needs a date check inside the job. Implementations differ, so confirm the semantics of the target machine before shipping an ops script.
#A timestamp converts to a time that is 8 hours off.
Because reverse conversion uses the browser's local timezone. Fold the offset in before converting, or shift mentally when reading server-side times. The tool deliberately offers no timezone selector.
#Why does a date show up as 1970?
A seconds value was treated as milliseconds. Ten-digit seconds must be multiplied by 1000; using them raw lands in January 1970. Going the other way, a 13-digit value read as seconds produces a date hundreds of thousands of years out.
#Are timestamps still usable after 2038?
On 64-bit systems, yes. The risk is wherever a timestamp lives in a signed 32-bit integer: an INT database column, an embedded device, an older runtime. Check whether that column is INT rather than BIGINT.
#Does a valid ID check digit prove the number is real?
No. The check digit is arithmetic that anyone can reproduce, so it only catches mistyped digits; it cannot prove existence. Verifying a real ID requires police or carrier records.