Guide — dev
Unix time, the working model
One number names one instant, the same instant everywhere on Earth. What a Unix timestamp actually counts, the three confusions behind nearly every timestamp bug, and the rules that keep them out of your system.
The short version: a Unix timestamp is one number: the count of seconds since 1970-01-01T00:00:00 UTC, not counting leap seconds. It names a single instant, the same instant everywhere on Earth, and time zones only change how that instant gets written out. Nearly every timestamp bug is one of three confusions: wrong unit (seconds vs milliseconds), wrong layer (instant vs wall clock), or wrong storage width (32 vs 64 bits).
What is a Unix timestamp, exactly?
POSIX defines "Seconds Since the Epoch" with a formula built from UTC calendar fields, and the formula gives every day exactly 86,400 seconds. That has a strange consequence: Unix time is not a true count of elapsed physical seconds. When UTC inserts a leap second, the real world ticks one more SI second than Unix time ever records. Unix time is a calendar encoding disguised as a counter, and for almost all software that is exactly what you want, because it makes date math predictable.
The epoch itself, midnight UTC on 1 January 1970, is arbitrary but universal. date +%s in a shell, Math.floor(Date.now() / 1000) in JavaScript, and int(time.time()) in Python all produce the same number at the same moment, anywhere on the planet. That agreement is the entire value of the format.
Seconds, milliseconds, or nanoseconds — which am I looking at?
Systems disagree on the unit, and this is the single most common source of pain.
- Seconds: C time(), most REST APIs, JWT exp claims, Python time.time() (as a float with fractional seconds).
- Milliseconds: JavaScript Date.now() and everything in a JS Date, Java System.currentTimeMillis(), many message queues.
- Microseconds: PostgreSQL's internal timestamp resolution, plenty of tracing systems.
- Nanoseconds: Go's time package internally, Linux clock_gettime, some columnar formats.
The digit count usually tells you the unit, because every value in the current era lands in a distinct band:
| Digits | Unit | Example | Meaning |
|---|---|---|---|
| 10 | seconds | 1787011200 | 2026-08-18T00:00:00Z |
| 13 | milliseconds | 1787011200000 | same instant |
| 16 | microseconds | 1787011200000000 | same instant |
| 19 | nanoseconds | 1787011200000000000 | same instant |
The heuristic holds because 10-digit second values span 2001 through 2286, and each finer unit just appends three digits. It breaks at the edges: an 11-digit number is either seconds in the far future (through the year 5138) or milliseconds from early 1973 or before, and there is no way to tell from the number alone. Our Unix Timestamp Converter takes the pragmatic line: up to 11 digits reads as seconds, 12 or more reads as milliseconds, and the output says which interpretation it chose so you can catch a wrong guess. It shows local, UTC, and ISO 8601 side by side from the same instant, which is the fastest way to spot a unit or zone mistake.
Why did my timestamp parse to 1970, or the year 58,000?
Both symptoms are the same bug, run in opposite directions.
Feed a seconds value to a milliseconds parser and it lands just after the epoch. new Date(1787011200) treats the number as milliseconds, about 20.7 days, and gives you 21 January 1970. Any time you see a date in early 1970, you handed seconds to something expecting milliseconds.
Feed a milliseconds value to a seconds parser and it flies tens of thousands of years out. Treat 1,787,011,200,000 as seconds and you get roughly 56,600 years past 1970, somewhere around the year 58,600. Screenshots of "your session expires in the year 54,000" are this bug, every time. The fix is never to touch the date; it is to divide or multiply by 1,000 at exactly one place, the boundary where the value enters your system.
Can timestamps be negative?
Yes. The epoch is a zero point, not a floor: −1 is 1969-12-31T23:59:59Z, and −86400 is the last day of 1969. JavaScript handles negative values across its whole supported range of ±8,640,000,000,000,000 milliseconds, about ±273,790 years around the epoch. Two caveats. Some languages and database functions reject negative inputs even where the platform supports them, so test before relying on it. And software applies the proleptic Gregorian calendar backwards indefinitely, so a "timestamp" for anything before modern calendar adoption is a useful convention rather than what any clock or calendar of the period would have shown.
What about leap seconds?
UTC occasionally inserts an extra second (a minute reading 23:59:60) to stay within 0.9 seconds of UT1, the timescale of Earth's actual rotation. Twenty-seven have been inserted since 1972; the most recent ended 2016. Unix time cannot represent 23:59:60, so real systems do one of two things. Step: the timestamp repeats a second, and naive code sees time stand still or jump. Smear: no second repeats because the clock runs fractionally slow across a window — Google and AWS smear linearly over 24 hours, noon to noon UTC around the leap. Either way, subtracting two wall-clock timestamps across a leap second can be off by one second, which is why elapsed-time measurement belongs on a monotonic clock, never on wall-clock differences.
This machinery is being retired. In November 2022, the CGPM (the body behind the SI system) resolved to let the UT1−UTC difference grow past one second by or before 2035, which in practice ends leap second insertion for at least a century; the ITU's WRC-23 recognized the decision, and the exact new tolerance is still being worked out between the metrology and radiocommunication bodies. As of this writing no leap second has been scheduled since 2016. Earth's rotation has actually been running fast in recent years, which put a never-before-tried negative leap second on the table as a possibility before 2035; treat that as a monitored risk rather than a prediction.
What is the 2038 problem, really?
A signed 32-bit integer tops out at 2,147,483,647. As a seconds count, that is 2038-01-19T03:14:07Z. One second later the value wraps negative, and the timestamp reads 1901-12-13T20:45:52Z. That is the whole problem: not a mystery, just a stored width.
The honest status in 2026 is that mainstream platforms are done or nearly done. Linux gained 64-bit time_t on 32-bit architectures in kernel 5.6 (2020), musl made it the default in 1.2.0, glibc 2.34 offers it via _TIME_BITS=64, and Debian 13 rebuilt over a thousand library packages to switch its 32-bit ports to 64-bit time, the largest ABI transition in the project's history. On disk, ext4 extended its timestamps to the year 2446 and XFS to 2486. A 64-bit seconds counter covers roughly ±292 billion years, so this migration is the last of its kind.
Where it still bites: embedded controllers and firmware with 20-to-30-year service lives, 4-byte time fields frozen into binary file formats and network protocols, MySQL's TIMESTAMP column type (still limited to the 32-bit range, unlike DATETIME), older 32-bit Android devices, and any struct someone declared as int32 in 2009 and never revisited. The overflow lives wherever a timestamp is stored or transmitted in 32 bits, so a fully 64-bit server can still corrupt data through a single 4-byte field. It also fires early: anything computing future dates, like 20-year certificate expiries or loan schedules, crosses the boundary years before 2038 does.
How do time zones and DST interact with timestamps?
They don't, and internalizing that prevents a whole family of bugs. A timestamp is a point on the global timeline. A time zone is a display rule that turns the point into local wall-clock text, and daylight saving is just that rule changing its offset twice a year. From this one idea, the classics fall out:
- Double conversion. Adding your UTC offset to a timestamp "to make it UTC" corrupts it, because it was already UTC by definition. If your times are wrong by exactly your local offset, this is what happened.
- Storing local wall time without a zone. 2026-11-01 01:30 in Chicago happens twice that night. A bare local string is ambiguous data.
- DST arithmetic. Adding 86,400 seconds to 9am Monday can give 8am or 10am Tuesday across a transition, and some wall times (2:30am on a spring-forward night) simply never occur. A local day is not always 86,400 seconds.
- The calendar-event rule. Past instants (logs, payments, measurements) want timestamps, stored in UTC. Future civil events want wall time plus an IANA zone name (09:00 Europe/Vilnius), with the timestamp computed as late as possible. Governments change zone rules on short notice, and if you precomputed the timestamp, a rule change silently moves everyone's meeting.
ISO 8601 or RFC 3339 — what should I write?
RFC 3339 is the strict internet profile of ISO 8601: a complete date and time, a T separator, and a mandatory offset, either Z or ±hh:mm. ISO 8601 also permits things RFC 3339 forbids, like week dates, ordinal dates, durations, intervals, reduced precision, and timestamps with no offset at all. The practical rule: emit RFC 3339 in UTC, as in 2026-08-18T09:00:00Z, and you satisfy both standards while staying machine-sortable as plain text; accept input more liberally. One quirk worth knowing: in RFC 3339, -00:00 means the offset to local time is unknown, while +00:00 and Z positively assert UTC.
What is the IANA time zone database?
The tzdb is the reference database of the world's zone rules, historical and future, keyed by names like Europe/Vilnius and America/New_York. It is maintained by a volunteer community under IANA, currently coordinated by Paul Eggert, with several releases a year as governments adjust their rules. Browsers ship it (via the ICU library) and expose it through the Intl API, which is why a static page can do correct zone math with no server and no bundled dataset. Our Time Zone Planner works exactly this way: it formats one instant through Intl.DateTimeFormat once per city, so DST comes out right in every zone for as long as the browser is reasonably current. When a country scraps DST, a browser update corrects the page with no deploy on our side.
Try it on a real value: paste any timestamp into the converter and see local, UTC and ISO 8601 side by side, with the unit detection labelled so a wrong guess is visible.
Open Unix Timestamp ConverterFair questions
Is Unix time in UTC?
It is defined against UTC and has no zone of its own. The same number displays as different wall times in different zones, all naming one instant.
Do Unix timestamps include leap seconds?
No. The POSIX formula gives every day 86,400 seconds, so the count skips them by construction.
What is the maximum date?
Signed 32-bit: January 2038. Signed 64-bit seconds: about 292 billion years. A JavaScript Date: about ±273,790 years around 1970.
Should I store integer timestamps or ISO strings?
Both name an instant, so either works if you are consistent. Integers compare, sort, and index cheaply; RFC 3339 UTC strings are readable in a log and still sort correctly as text. Pick one per system and convert at the edges.
The working rules
- A timestamp is an instant; zones are display. Convert at the boundary, exactly once.
- Know your unit before you parse: in this era, 10 digits means seconds and 13 means milliseconds.
- Store the past as UTC timestamps. Store future civil events as wall time plus an IANA zone name.
- Measure elapsed time with a monotonic clock, never by subtracting wall-clock readings.
- Emit RFC 3339 with Z. Accept more, produce one form.
- Audit every place a time value is stored or transmitted in 32 bits, especially formats and firmware.
- Read the symptoms: off by your local offset means double conversion, a 1970 date means seconds fed to a milliseconds parser, a five-digit year means the reverse.
Sources and further reading
- Wikipedia: Year 2038 problem — the overflow arithmetic and the platform-by-platform migration record.
- musl: time64 release notes — 64-bit time_t as the default from musl 1.2.0.
- Debian 13 release notes — the 64-bit time_t switch on Debian's 32-bit ports.
- debian-devel-announce: the time_t transition — the announcement of Debian's largest cross-archive ABI transition.
- CloudNews: Y2038 status in 2026 — where remaining 2038 exposure concentrates.
- Royal Institute of Navigation: leap seconds phased out by 2035 — the CGPM 2022 resolution and what it changes.
- timeanddate.com: future leap seconds — the insertion history since 1972 and the negative leap second discussion.
- Google: leap smear — the 24-hour noon-to-noon linear smear, also used by AWS.