The Complete Guide to Timestamps, Time Zones and Cron
Time looks like the easiest data type in your system until the first bug report arrives: a report that runs an hour early every spring, a nightly job that fires twice on one November morning, a user in Auckland who sees yesterday's date on today's invoice. None of that is exotic. It is what happens when a program treats a wall-clock reading as if it were a fixed point on the timeline, or treats a stored offset as if it were a time zone. Handling dates in code correctly comes down to a small set of distinctions that, once you have them, stay useful in every language and every database. This guide covers all of them: what a Unix timestamp actually measures, why time zones are political rather than arithmetic, how to pick a storage format you will not regret, and how cron turns five numbers into a schedule. Every concept here has a free browser-based tool on this site to test it against, and none of what you paste into those tools leaves your machine.
The Epoch: One Number, One Instant
A Unix timestamp is the count of seconds elapsed since 1970-01-01T00:00:00 UTC, the moment known as the Unix epoch. Right now that number is somewhere north of 1.7 billion and rising by one every second. Times before 1970 are represented as negative numbers, which is how the format handles birthdays in 1962 or historical records from the nineteenth century.
The single most important property of a timestamp is that it is time zone agnostic. The value 1785067200 refers to exactly one instant in the history of the universe. Someone in Tokyo and someone in São Paulo who read that number at the same moment are reading about the same moment. The time zone enters only when you render the number as text — the same value formats as 2026-08-07 00:00:00 in UTC, 2026-08-06 20:00:00 in New York, and 2026-08-07 09:00:00 in Tokyo. Three strings, one instant. This is precisely why timestamps are such a good transport and storage format: there is nothing left to misinterpret.
Seconds Versus Milliseconds
The original Unix convention counts seconds, and that is what you get from date +%s in a shell, time.time() in Python (as a float), and the UNIX_TIMESTAMP() function in MySQL. JavaScript broke ranks: Date.now() and new Date().getTime() return milliseconds since the same epoch. Java's System.currentTimeMillis() does the same, and many APIs and log formats followed.
The practical rule is digit counting. A 10-digit value is seconds; a 13-digit value is milliseconds. If a converted date comes out on 1970-01-20, you passed milliseconds into something expecting seconds. If it comes out in the year 56000, you did the opposite. Microsecond (16-digit) and nanosecond (19-digit) timestamps also exist — Python's time.time_ns() and Go's UnixNano() produce the latter. Paste an unknown value into the Unix Timestamp Converter and let it detect the unit rather than guessing.
What Unix Time Deliberately Ignores
Unix time as specified by POSIX counts non-leap seconds. Leap seconds — the occasional extra second inserted into UTC to keep it aligned with the Earth's rotation — are not represented. When one occurs, Unix time either repeats or stretches a second depending on the system's smearing strategy. For almost every application this is invisible and irrelevant; for high-precision timing systems it is a known hazard worth reading about separately.
Unix time also has no concept of a calendar. It does not know about months of unequal length, leap years, or the fact that some countries have skipped days entirely when switching calendars. All of that lives in the formatting layer, which is exactly where it belongs. For a deeper walk through the format itself, see What Is a Unix Timestamp?.
Time Zones Are Not Offsets
This is the distinction that fixes most date bugs. An offset is a number: +05:30, -08:00. A time zone is a named region with a history and future of rules about which offset applies at any given moment. Asia/Kolkata is a time zone; +05:30 is the offset it happens to use. America/Los_Angeles is a time zone; it uses -08:00 in January and -07:00 in July.
Storing -08:00 tells you what a timestamp meant at the moment it was written. It does not let you schedule anything in the future, because you cannot know whether the offset still applies next March. Only the zone name carries that information.
The IANA Time Zone Database
The rules live in the IANA time zone database (also called tz or zoneinfo), the source of truth that ships with operating systems, browsers, JVMs, and language runtimes. Its identifiers look like Area/Location: Europe/Berlin, America/New_York, Australia/Sydney, Pacific/Auckland. It records not only current rules but every historical change — when a country moved zones, when it abolished DST, when a colonial-era offset with an odd number of minutes was retired.
Because governments change these rules on political timescales, the database is updated several times a year. Egypt reintroduced DST in 2023 after abandoning it, and several Pacific nations have shifted the international date line around them. That means a running system's date logic can go stale: if your container image ships a two-year-old tzdata, future conversions for affected zones will be wrong. Keeping tzdata current is a maintenance task, not a one-time setup.
Avoid the three-letter abbreviations — EST, CST, IST. They are ambiguous (CST is Central Standard Time in North America, China Standard Time, and Cuba Standard Time) and they do not encode DST behaviour. Use them in display strings if you must, never as data. The World Timezone Converter & Clock works from IANA names for this reason.
Daylight Saving Time and the Two Broken Hours
Twice a year, DST regions produce local times that are not well-behaved:
- Spring forward creates a gap. In the United States, clocks jump from 02:00 to 03:00 on the second Sunday in March, so
2026-03-08 02:30local simply does not exist inAmerica/New_York. Naively parsing it will either throw, silently shift, or produce garbage depending on the library. - Fall back creates a repeat. On the first Sunday in November, 01:30 happens twice. A local timestamp of
2026-11-01 01:30is genuinely ambiguous — it maps to two distinct instants an hour apart.
Different regions do this on different dates: the European Union switches on the last Sundays of March and October, at 01:00 UTC across all member states simultaneously, so the local clock hour varies by country. Southern-hemisphere zones shift in the opposite months. There is no shortcut here; you either use a library backed by tzdata or you get it wrong.
Picking a Format You Will Not Regret
Three formats are worth using, and a long tail of formats worth refusing.
ISO 8601 / RFC 3339 is the readable default: 2026-08-07T14:30:00Z, where the trailing Z ("Zulu") means UTC, or 2026-08-07T20:00:00+05:30 for an explicit offset. It sorts correctly as a plain string, it is unambiguous, and every language can parse it. Use it in JSON APIs, logs, and config files.
Unix timestamps are the compact default: smaller, trivially comparable, and impossible to misparse. Use them in databases, caches, protocol buffers, and anywhere storage or arithmetic matters more than readability. Convert them for humans at the edge with the Unix Epoch to Date Formatter.
A local date with a zone name, stored as two fields (2026-12-25T09:00:00 plus America/Chicago), is the right choice for future civil events: a recurring meeting, a store opening time, an alarm. The user means "9am local whatever the rules say then," and only the zone name preserves that intent.
What to refuse: MM/DD/YYYY versus DD/MM/YYYY — 03/04/2026 is two different days depending on the reader's country. Bare local datetimes with no zone and no offset. Excel serial dates leaking into an API. Anything with a three-letter zone abbreviation as its only zone information.
Storage Rules That Hold Up
Four rules cover most systems:
- Store instants in UTC. Convert on the way in, convert back on the way out. In PostgreSQL,
timestamptznormalises to UTC internally and is the right default; plaintimestampstores a naked wall-clock reading with no zone at all. In MySQL,TIMESTAMPconverts using the session time zone whileDATETIMEdoes not, which is a frequent source of surprise. - Keep the user's zone as a separate column. You need it to render correctly and to schedule future events. Storing a UTC instant does not tell you where the user was.
- Do date arithmetic in the target zone, not in UTC. "The start of tomorrow" for a user in Sydney is not "now plus 24 hours" and it is not midnight UTC. Convert to the zone, compute the calendar boundary there, then convert back.
- Set servers and containers to UTC. It removes an entire class of "works on my machine" bugs and makes log correlation across regions trivial.
The full argument, with the specific bugs each rule prevents, is in UTC vs Local Time: Storing Dates Without Regret.
Cron: Scheduling in Five Fields
Cron is the scheduling language you will meet on every Unix system, in Kubernetes, in CI pipelines, and in most job runners. A standard expression is five space-separated fields:
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-6, Sunday = 0)
│ │ │ │ │
* * * * *
Many implementations also accept 7 for Sunday, and three-letter names (JAN-DEC, SUN-SAT). Quartz, Spring, and some cloud schedulers prepend a seconds field, giving six — check which dialect you are writing for before copying an expression between systems.
Four operators do all the work:
| Operator | Meaning | Example | Reads as |
|---|---|---|---|
* |
every value | * * * * * |
every minute |
, |
a list | 0 9,17 * * * |
09:00 and 17:00 daily |
- |
a range | 0 9 * * 1-5 |
09:00 Monday through Friday |
/ |
a step | */15 * * * * |
every 15 minutes |
So 0 9 * * 1-5 means: minute 0, hour 9, any day of month, any month, weekdays only — nine in the morning on working days. 30 2 1 * * is 02:30 on the first of every month. 0 0 * * 0 is midnight on Sundays.
One genuine trap: when both day-of-month and day-of-week are restricted (neither is *), Vixie-style cron treats them as an OR, not an AND. 0 0 13 * 5 fires on the 13th of every month and on every Friday — not only on Friday the 13th. This surprises almost everyone the first time.
Cron's other silent variable is the time zone. The daemon uses the system zone unless told otherwise; Vixie cron reads a CRON_TZ= line at the top of a crontab, and Kubernetes CronJobs take a spec.timeZone. A job set to 0 2 * * * in a DST-observing zone will skip a run in spring and run twice in autumn. Scheduling in UTC avoids that, at the cost of the local hour drifting by one twice a year.
For the field-by-field walkthrough see How to Read a Cron Expression, and for ready-made expressions see The Cron Schedules You'll Actually Use. Before you commit anything to a crontab, paste it into the Cron Expression Parser & Next Run Times and read the next few fire times — an expression that is syntactically valid can still be semantically wrong, and the fire times tell you instantly.
The Bugs You Are Most Likely to Ship
- Comparing a local time to a UTC time. Always normalise both sides to instants first.
- Assuming offsets are whole hours. India is
+05:30, Nepal is+05:45, parts of Australia are+08:45. Integer-hour arithmetic silently breaks for hundreds of millions of people. - Assuming a day is 86,400 seconds. On DST transition days it is 82,800 or 90,000 in the affected zone.
- Hand-rolling leap year logic. The rule is: divisible by 4, except centuries, unless divisible by 400. 2000 was a leap year, 1900 was not, 2100 will not be.
- Truncating precision on the way in. If you store seconds and the source had milliseconds, events that were ordered become tied.
- Trusting client clocks. Browser and device clocks drift and can be set arbitrarily. Timestamp on the server for anything that matters.
- Assuming 32-bit time still fits. It stops fitting on 2038-01-19; see The Year 2038 Problem Explained for how much this actually affects you.
Conclusion
The whole subject reduces to one habit: keep the instant and its presentation separate in your head and in your schema. An instant is a Unix timestamp or a UTC ISO 8601 string — one unambiguous point, safe to store, compare, and transmit. A presentation is that instant rendered through an IANA time zone for a specific human, computed as late as possible and never stored as if it were the truth. Future civil events are the one exception, and they get a zone name stored alongside them precisely so the instant can be recomputed when the rules change. Cron sits on top of all this: five fields, four operators, and a time zone you should always state rather than inherit. Work through the supporting posts in this cluster for the details, and use the browser-based timestamp, time zone, and cron tools to check your assumptions before they reach production.
Frequently asked questions
What exactly is a Unix timestamp?
It is the number of seconds that have elapsed since 1970-01-01T00:00:00 UTC, known as the Unix epoch. Some systems count milliseconds instead. The number identifies a single instant on the global timeline and carries no time zone of its own — a time zone is applied only when you render it as a human-readable date.
Is a Unix timestamp in UTC or local time?
Neither, strictly speaking. A timestamp is an offset from a fixed reference instant, so it is time zone independent. It is commonly described as UTC because the epoch it counts from is defined in UTC, and because converting it with no time zone applied gives you the UTC rendering.
How do I tell whether a timestamp is in seconds or milliseconds?
Count the digits. A 10-digit value is seconds and lands somewhere between 2001 and 2286. A 13-digit value is milliseconds and lands in roughly the same era. If a date comes out in 1970, you fed milliseconds to something expecting seconds; if it lands tens of thousands of years in the future, you did the reverse.
Why should I store dates in UTC instead of local time?
UTC never shifts, so the same stored value always means the same instant. Local times shift twice a year in DST regions, and a stored local time with no offset becomes ambiguous during the fall-back hour and impossible during the spring-forward hour. Store UTC, convert to local only when displaying.
What are the five fields in a cron expression?
In order: minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-6, where Sunday is 0, and many implementations also accept 7 for Sunday). Some schedulers such as Quartz and Spring add a leading seconds field, making six.
What time zone does a cron job run in?
By default, the local time zone of the machine or scheduler running it, which is often UTC on servers and containers. Vixie cron supports a CRON_TZ variable at the top of a crontab, and Kubernetes CronJobs accept a spec.timeZone field. If a job must fire at a fixed local hour year-round, set the zone explicitly.
Does UTC observe daylight saving time?
No. UTC has no DST and no seasonal adjustment; it is the reference against which every civil time zone defines its offset. Regions that observe DST change their offset from UTC twice a year — for example New York moves between UTC-5 and UTC-4 — while UTC itself never moves.
Is the year 2038 problem still something I need to worry about?
Rarely on modern systems. The bug affects 32-bit signed time values, which overflow at 2038-01-19T03:14:07 UTC. Current 64-bit operating systems, languages and databases already use wider types. The remaining risk sits in long-lived embedded hardware, 32-bit builds, and file or protocol formats that hard-code a 32-bit field.
Try the related tools
Unix Timestamp Converter
Convert Unix epoch timestamps (seconds & ms) to UTC, ISO 8601, and local dates.
Unix Epoch to Date Formatter
Convert numeric Unix timestamps into multiple human-readable date formats.
World Timezone Converter & Clock
Convert time between major world timezones (UTC, EST, PST, GMT, IST, JST, CET).
Cron Expression Parser & Next Run Times
Parse 5-field cron expressions and calculate upcoming execution schedules.