August 9, 2026
Unix timestamps and time zones: a complete guide for developers
Every log line, every database row, every JWT exp claim, every cache TTL you have ever written encodes a moment in time. And sooner or later one of them bites: a log “from the future” by eight hours, a cron job firing twice, a token that expires while the user is still logged in. Almost every one of those bugs traces back to the same root: confusing an instant with a wall-clock reading, or mixing up who is responsible for the time zone — your code, the database, or the browser.
This guide walks through the whole model from first principles: what a Unix timestamp counts, why UTC is a convention and not a time zone, how ISO 8601 spells a timestamp, where daylight saving time (DST) breaks naive arithmetic, and how to keep distributed systems and distributed teams sane. Every conversion shown here is a real value you can double-check with the timestamp converter and the world clock on this site — both run entirely in your browser.
What a Unix timestamp actually is#
A Unix timestamp is a single integer: the number of seconds elapsed since 1970-01-01T00:00:00Z, a moment nicknamed “the epoch”. That is the entire contract.
Two properties follow, and they explain most of the format’s charm:
- It has no time zone. The number
1723455667denotes one specific instant, full stop. It is simultaneously 09:41 in London and 17:41 in Shanghai. The time zone only appears when a human wants to read the value, at which point you render the instant into some region’s wall clock. - It has no calendar. There is no month, no weekday, no “day boundary”. If you need “all events from Tuesday”, you are asking a calendar question, and the timestamp must first be rendered into a specific zone’s calendar before “Tuesday” means anything.
The classic reference points, all verifiable in the converter:
| Timestamp | UTC rendering | Note |
|---|---|---|
0 | 1970-01-01T00:00:00Z | The epoch itself |
1000000000 | 2001-09-09T01:46:40Z | One billion — a Sunday |
1234567890 | 2009-02-13T23:31:30Z | The “funny digits” moment |
1723455667 | 2024-08-12T09:41:07Z | Our worked example below |
2000000000 | 2033-05-18T03:33:20Z | Two billion |
2147483647 | 2038-01-19T03:14:07Z | The 32-bit ceiling |
That last row is the famous year 2038 problem: a signed 32-bit integer tops out at 2147483647 seconds past the epoch. One second later it wraps negative and, on systems that still store time that way, reads as December 1901. Mainstream 64-bit platforms use 64-bit time_t and are safe for 292 billion years; the risk lives on in legacy embedded systems, old file formats, and wire protocols that define fields as int32. If you design a binary or database schema today, make timestamp fields 64-bit — or store ISO 8601 strings.
Seconds or milliseconds? The 1e12 rule#
Java (System.currentTimeMillis()), JavaScript (Date.now()), and most log aggregators use milliseconds; Unix tools, Python’s time.time() integer part, and most APIs use seconds. Mixing them up produces dates tens of thousands of years off, which at least fails loudly.
A practical heuristic — and the one this site’s converter uses: if the absolute value is below 1,000,000,000,000 (1e12), it is seconds; otherwise it is milliseconds. Ten-digit second-epochs stay below 1e12 until the year 2286, and millisecond values crossed 1e12 back in September 2001 (recall from the table above: 1000000000 seconds is 2001, and 1000000000000 milliseconds is the same instant), so the ranges never overlap in any realistic application. Try it: paste 1723455667 into the converter and you get August 2024; paste 1723455667000 and you get the same date — the tool detects the unit for you.
UTC, GMT, and local time: untangling the vocabulary#
Developers use these words interchangeably, and that is exactly where bugs hide. The precise meanings:
- UTC (Coordinated Universal Time) is the global time standard: the coordinate system every clock on Earth agrees on. It is not a time zone in the IANA sense — there is no
Europe/UTC-style DST rule attached to it. When people say “store in UTC” they really mean “store the unambiguous instant, with no region’s offset baked in”. - GMT (Greenwich Mean Time) is the ancestor of UTC. In practice the two differ by less than a second, and the label
GMT+00:00survives in formatted output (you will see it in email headers andIntloutput) as a synonym for a zero offset. Fine to read, slightly sloppy to write in new designs. - An offset (
+08:00,-05:00) is just a number: “this wall clock is N minutes away from UTC”. It contains no rules. - A time zone is a region plus its rules: which offset applies when, and when DST flips. Zones have IANA identifiers like
Asia/Shanghai,America/New_York,Europe/Berlin. One zone implies different offsets at different times of the year; one offset never tells you which zone you are in.
The practical rule that prevents a whole class of bugs: store and transmit instants (timestamps or UTC-stamped ISO 8601); convert to a zone only at the display edge, using the viewer’s preference. Databases should hold UTC. Logs should hold UTC or epoch seconds. APIs should exchange UTC. The user’s browser knows the user’s zone and locale far better than your server ever will — let it do the final rendering.
Half-hour and 45-minute zones#
If you grew up on whole-hour offsets, the world is messier than expected. India (Asia/Kolkata) is UTC+05:30; Nepal (Asia/Kathmandu) is UTC+05:45. At instant 1723455667, when it is 17:41:07 in Shanghai and 09:41:07 in London, Kolkata reads 15:11:07 — the odd half hour is visible in the minutes. Any code that assumes offsets are whole hours (“diff of local hours times 3600”) silently corrupts times for a billion-plus people. Offsets in minutes, like the world-clock tool computes, are the safe representation.
ISO 8601: the string format that removes ambiguity#
Raw integers are compact but opaque to humans, so APIs overwhelmingly exchange timestamps as ISO 8601 strings. The grammar worth memorizing, from the same instant 1723455667:
2024-08-12T09:41:07Z UTC, "Z" = Zulu time = zero offset
2024-08-12T09:41:07+00:00 the same instant, explicit offset
2024-08-12T17:41:07+08:00 the same instant, Shanghai wall clock
2024-08-12T09:41:07.000Z with milliseconds
Three things to burn into memory:
Zis load-bearing.2024-08-12T09:41:07Zand2024-08-12T09:41:07(no suffix) are different things. The first names a global instant; the second is a naked wall-clock reading — and what a parser does with it varies. JavaScript’sDate.parsetreats a suffix-less ISO date-time as local time when it has a time component (and as UTC when it is date-only, the infamous asymmetry); other languages and libraries default differently. Always emit the offset, always.- The offset does not identify a zone.
+08:00is used by Shanghai, Singapore, Hong Kong, Perth, and Irkutsk — regions with different DST histories. An offset answers “how far from UTC right now”; a zone name answers “what the rules are”. When a user configures a calendar, you want a zone; when you serialize an instant, an offset is enough. - RFC 2822 is its email-era cousin. You will meet
Mon, 12 Aug 2024 09:41:07 +0000in emailDate:headers. Readable, but month names are English and the format predates Unicode — prefer ISO 8601 in anything you design. The timestamp converter renders both side by side so you can see the correspondence.
A formatting tip: the same instant renders differently per locale (08/12/2024 versus 12.08.2024 for August 12). Never re-parse a displayed date string; keep the machine representation (epoch or ISO) as the source of truth and format from it each time.
Daylight saving time: where naive code breaks#
DST is the reason “just add 3600 × 24 to move to tomorrow” is wrong. On spring-forward night a zone has 23 hours; on fall-back night it has 25. Two concrete failures, using America/New_York in 2024 (verified with the world-clock tool):
- Spring forward, 2024-03-10, 02:00 → 03:00. The UTC instant
2024-03-10T06:59:00Zis 01:59 EST (offset −05:00) in New York. Two minutes later,2024-03-10T07:01:00Zis 03:01 EDT (offset −04:00). Wall-clock time jumped from 01:59 straight to 03:01 — 02:00 through 02:59 never existed that day. Any local-time arithmetic that lands in the gap has no answer, and databases either error or silently shift forward. - Fall back, 2024-11-03, 02:00 → 01:00. At
2024-11-03T05:59:00ZNew York reads 01:59 EDT (−04:00); at2024-11-03T06:01:00Zit reads 01:01 EST (−05:00). The wall clock ran 01:00–01:59 twice. “Meet at 01:30” that night is ambiguous — an instant must be chosen. This is exactly why schedulers that walk in wall-clock minutes can fire an hourly job twice.
The professional habits that survive DST:
- Do arithmetic in UTC or on epoch values, then render for display. “Now + 90 minutes” is correct in epoch space regardless of DST.
- The “add one day” exception. For human semantics — “same time tomorrow” — you often want wall-clock behavior (23- or 25-hour days included). Libraries express this with calendar-level arithmetic (“add 1 day in zone”) versus duration arithmetic (“add 86,400 seconds”). Choose deliberately.
- Never derive a zone from an offset. During summer,
America/New_YorkandAmerica/Chicago-in-winter share offsets with other zones; a stored−04:00tells you nothing about December. Store the IANA zone ID when future conversions are needed. - Beware zone rules changing under you. Governments edit DST laws; 32-bit
time_tsystems with frozen tzdata get it wrong forever. Keep tzdata (via your OS or ICU) updated — browsers ship updated rules and the world-clock tool reads them live throughIntl.
One more trap that catches even seniors: Asia/Shanghai is UTC+8 today and has no DST, so Chinese deployments often hardcode +08:00. The moment that code serves users in Australia/Sydney (UTC+10 standard, UTC+11 with DST) — visible in the worked example below, where Sydney is ahead of Tokyo in August but behind it in February — hardcoded offsets start producing scheduling drift of a full hour for part of the year.
Worked example: one instant, five cities#
Let us trace one timestamp end to end, exactly as the site’s tools do. Take 1723455667 from a log file — ten digits, so by the 1e12 rule it is seconds.
- Paste it into the timestamp converter. It parses to the instant
2024-08-12T09:41:07Z, and the tool shows all representations at once: ISO 8601 (2024-08-12T09:41:07.000Z), RFC 2822 (Mon, 12 Aug 2024 09:41:07 +0000), Unix seconds and milliseconds (1723455667/1723455667000), a UTC rendering, a local rendering for your own zone, and a relative label (“over a year ago” as of this writing). - Note it was a Monday in UTC. That calendar fact — invisible in the raw integer — already differs across zones: at
2024-08-12T01:00:00Zit is Monday in Shanghai but still Sunday evening in New York. “Group by day” without fixing a zone scatters events across two buckets. - Now open the world clock, switch to reference-time mode, and enter
2024-08-12T09:00with Shanghai as the anchor zone. Every row re-renders the same instant in its own zone (August = northern summer / southern winter):
Asia/Shanghai Mon 09:00 UTC+08:00
Asia/Tokyo Mon 10:00 UTC+09:00
Australia/Sydney Mon 11:00 UTC+10:00 (southern winter standard time)
Europe/Berlin Mon 03:00 UTC+02:00 (summer time, CEST)
Europe/London Mon 02:00 UTC+01:00 (summer time, BST)
Asia/Kolkata Mon 06:30 UTC+05:30 (half-hour zone)
America/New_York Sun 21:00 UTC−04:00 (previous day!)
Read that last line twice: a perfectly reasonable “Monday morning standup” in Shanghai is Sunday night in New York. This is the entire reason the world-clock tool exists — it turns “what time is that for them” into one glance, including work-hours badges per zone so you can see immediately that New York is outside 09:00–18:00.
Practical defaults for servers, databases, and APIs#
Condensed into rules you can apply today:
- Store epoch seconds/milliseconds or ISO 8601 with an explicit offset (
Z). 64-bit fields, never int32. If the database has a timestamp type, keep it in UTC. - Transmit ISO 8601 with the offset always present. If the counterparty needs a zone-aware value (a calendar event), send both the UTC instant and the IANA zone ID so their renderer resolves it correctly.
- Render at the edge, in the user’s zone and locale, from the stored instant. In JavaScript that means
Intl.DateTimeFormatwithtimeZone— the same engine facility both site tools use — not hand-rolled offset math. - Test with DST-edge dates: the Sunday of the transition, on both sides of 02:00 local, plus one half-hour zone (
Asia/Kolkata) and one southern-hemisphere zone (Australia/Sydney). If your scheduler handles those, it handles the calendar year. - Cron and schedules deserve special care: expressions like
0 9 * * 1-5mean “09:00 local to the system zone” — pick the zone explicitly instead of inheriting the container’s default, and check what your cron implementation does across a DST gap (skip, fire twice, or fire late). The cron expression explainer shows the next fire times so you can verify before trusting it.
FAQ#
Why does new Date("2024-08-12") give UTC but new Date("2024-08-12T09:41") give local time?#
The ECMAScript specification treats date-only ISO strings as UTC and date-time strings without an offset as local time. It is a documented asymmetry and a famous source of off-by-one-day bugs. The fix is to always include the offset (2024-08-12T09:41:07Z) or construct dates from explicit numeric parts.
Should I store timestamps as integers or ISO strings?#
Both are correct if handled properly. Integers are compact, index-friendly, trivially comparable, and unit-testable — but you must fix the unit (seconds vs milliseconds) in writing. ISO 8601 strings are human-readable, debuggable in logs, and self-describing about offset, at the cost of size and parsing discipline. Whichever you choose, be consistent across the whole system; mixed storage is where the 1000× errors come from.
How do I convert a Unix timestamp to “time ago” correctly?#
Compute the difference in seconds against the current instant, then format into the largest sensible unit (“5 minutes ago”, “2 hours ago”) using the viewer’s locale, since the phrasing differs per language. The timestamp converter on this site does exactly this with Intl.RelativeTimeFormat — try pasting any timestamp and watch the status line. Never format relative time on the server for later display; the “now” you compute with goes stale immediately.
Is UTC the same as GMT? And what is “Z”?#
For everyday programming, yes — GMT and UTC differ by at most a fraction of a second, and formatted output often says GMT where the underlying value is UTC. The Z suffix in ISO 8601 means “zero offset” (historically “Zulu time”). In new designs, say UTC in prose and emit Z or +00:00 in strings.
My log timestamps are off by exactly 8 hours (or 5, or 9). What happened?#
A fixed whole-hour offset between expectation and reality almost always means an offset was applied twice (or never): the server stored local time labeled as UTC, or the display layer applied the viewer’s zone to an already-zoned value. Find the boundary where the value is converted, and make exactly one layer responsible for zone conversion — the display edge.
Summary and tools#
The mental model fits in three sentences. A Unix timestamp is a zone-free, calendar-free count of seconds since 1970. UTC is the coordinate system, while zones are regional rule sets that map instants to wall clocks differently across the year. Store and compute in instants, render in the viewer’s zone, and distrust any arithmetic on wall-clock strings.
Practice it with the tools on this site:
- Timestamp converter — paste seconds, milliseconds, or an ISO string; get ISO 8601, RFC 2822, UTC, local, and relative time in one shot, with automatic unit detection.
- World clock — pin a reference time and compare any set of zones, with UTC offsets and work-hours badges for meeting planning.
- Cron expression explainer — check what a schedule actually means, including its next fire times, before you trust it with a production job.