Coding

Unix Timestamps Explained: Epoch Time, Time Zones, and the 2038 Problem

July 11, 2026 ยท 9 min read

A Unix timestamp is the number of seconds that have passed since 00:00:00 UTC on 1 January 1970. That moment is called the Unix epoch, and every timestamp is simply a running count from it. As of mid-2026 the count sits around 1.78 billion, which is why you see 10-digit numbers like 1783728000 in logs, APIs, and database rows.

That is the entire concept: one integer, one instant in time, no time zone attached. The reason it trips people up is that this clean idea collides with real-world messiness โ€” some systems count seconds and others count milliseconds, dates get displayed in local time even though the number is always UTC, and a very common way of storing that number runs out of room in the year 2038.

This guide walks through what epoch time actually is, why it starts in 1970, the seconds-versus-milliseconds bug that produces dates in 1970 or the year 55000, how time zones fit in, and exactly what the Year 2038 problem does. If you just need to convert one value, an epoch time converter will do it in a click โ€” but knowing how the number works keeps you from shipping the classic bugs.

What a Unix timestamp actually is

Unix time, epoch time, and POSIX time all mean the same thing: a single number counting seconds since the Unix epoch, which is midnight UTC at the start of 1 January 1970. A timestamp of 0 is that exact instant. A timestamp of 1000000000 is 2001-09-09 01:46:40 UTC. The number goes up by exactly one every second, everywhere on Earth at once.

Why 1970? When Unix was being built at Bell Labs in the early 1970s, the developers needed a fixed reference point to measure time from. An early version counted time in sixtieths of a second, which overflowed its 32-bit counter in only a couple of years, so they needed an epoch close to the present. They settled on the clean, round start of 1970 โ€” recent enough to be practical, simple enough to remember. It stuck, and today essentially every operating system, programming language, and network protocol speaks epoch time.

The key mental model: a Unix timestamp is a count, not a formatted date. It carries no year, month, day, hour, or time zone inside it. Those only appear when you convert the number into something human-readable. The same instant is the same timestamp whether you are in Tokyo or Toronto โ€” only the way you display it differs.

  • It is always measured in UTC โ€” there is no such thing as a New York timestamp versus a London timestamp.
  • It represents one exact instant, not a duration or a calendar date.
  • It can be negative: dates before 1970 count down (1969-12-31 00:00 UTC is -86400).
  • Bigger number means later in time; subtracting two timestamps gives the seconds between them.

Seconds vs milliseconds: the bug that shows up everywhere

Here is the single most common epoch-time bug. Classic Unix time counts seconds, but many environments count milliseconds. JavaScript is the famous example: Date.now() returns milliseconds since the epoch, a 13-digit number, while Python's time.time(), PHP's time(), and the shell's date +%s all return seconds, a 10-digit number. Mix the two and your dates land decades or millennia away from where they should.

Concretely: the seconds value 1700000000 is 2023-11-14 22:13:20 UTC. Feed that same number into JavaScript's new Date(1700000000) โ€” which expects milliseconds โ€” and it reads it as 1.7 billion milliseconds, about twenty days, giving you 1970-01-20, just after the epoch. Go the other way and pass a millisecond value into a function expecting seconds, and you get a date tens of thousands of years in the future.

The fastest sanity check is to count the digits. In this era a seconds timestamp is 10 digits and a milliseconds timestamp is 13. If a date comes out in 1970, you almost certainly divided milliseconds by nothing when you needed to divide by 1000; if it comes out absurdly far in the future, you multiplied seconds by 1000 when the target already wanted seconds.

UnitDigits (current era)Example valueCommon source
Seconds101783728000Unix date +%s, Python time.time()
Milliseconds131783728000000JavaScript Date.now(), Java System.currentTimeMillis()
Microseconds161783728000000000Python datetime, some databases
Nanoseconds191783728000000000000Go time.UnixNano(), high-resolution clocks

Converting timestamps to and from human dates

Converting is a two-step idea: turn the count into an instant, then format that instant in whatever time zone you want to show. Every language ships helpers for both directions. The table below shows how to read the current time as a Unix value and how to turn a stored timestamp back into a date object.

The one caveat that bites people: the naive parse-back function often defaults to the machine's local time zone. In Python, datetime.fromtimestamp(ts) uses local time, while datetime.fromtimestamp(ts, tz=timezone.utc) forces UTC โ€” pick the second one unless you deliberately want local. In JavaScript, new Date(ts * 1000) creates the correct instant, but methods like getHours() report it in the browser's zone while getUTCHours() reports UTC.

When you just need to check one value โ€” say a suspicious createdAt field in a log โ€” pasting it into a converter is faster and safer than eyeballing it, because it also tells you immediately whether you handed it seconds or milliseconds.

LanguageCurrent Unix time (seconds)Timestamp back to a date
JavaScriptMath.floor(Date.now() / 1000)new Date(ts * 1000)
Pythonint(time.time())datetime.fromtimestamp(ts, tz=timezone.utc)
PHPtime()date('c', $ts)
Gotime.Now().Unix()time.Unix(ts, 0)
Bash (GNU)date +%sdate -d @ts
Bash (macOS/BSD)date +%sdate -r ts
PostgreSQLextract(epoch from now())to_timestamp(ts)

Why a timestamp has no time zone

This is the point that clears up most time-zone confusion: the timestamp itself is always UTC, and a time zone is only ever applied when you display it. The number 1783728000 does not become a different number in Mumbai versus Los Angeles โ€” it is the same instant. What changes is that one place formats it as an evening and another as a morning of the same day.

Two things people conflate are worth separating. A fixed offset like -05:00 is just a number of hours from UTC. A named zone like America/New_York is a rule set that switches between -05:00 in winter and -04:00 during daylight saving time, and it knows the exact dates those switches happened historically. If you store an offset instead of a zone, you lose the ability to display past and future dates correctly across a DST change.

The durable best practice: store the instant as a Unix timestamp (or an ISO 8601 UTC string) and attach the display zone only at the last moment, in the interface. Storing local wall-clock times without a zone is how you end up with events that shift by an hour twice a year or bookings that are ambiguous during the hour a clock falls back.

The leap-second caveat most explanations skip

Unix time is often described as the number of seconds elapsed since 1970, but that is not strictly true, and the difference occasionally matters. Unix time assumes every day is exactly 86400 seconds. Real UTC does not: to keep clocks aligned with the Earth's slightly irregular rotation, timekeepers have inserted 27 leap seconds since 1972.

Unix time ignores those leap seconds entirely. When a leap second occurs, the Unix clock does not advance through a unique 61st second โ€” depending on the system it either repeats a value or pauses. So a Unix timestamp is not a perfect count of physical (SI) seconds since 1970; it is a count that stays locked to civil UTC dates instead.

For almost everything โ€” scheduling, logging, ordering events, showing a date โ€” this is invisible and you never need to think about it. It only becomes relevant in high-precision domains like scientific timing, financial trade sequencing at sub-second resolution, or satellite systems, where the handful of seconds of accumulated difference between Unix time and true elapsed time is significant.

The Year 2038 problem

The Year 2038 problem, sometimes called the Epochalypse, comes from how the timestamp is stored rather than the concept itself. For decades the standard storage type was a signed 32-bit integer. The largest value a signed 32-bit integer can hold is 2,147,483,647 โ€” and that many seconds after the epoch lands at exactly 03:14:07 UTC on 19 January 2038.

One second later the counter overflows. Instead of ticking up, it wraps around to the most negative value, which represents 13 December 1901. Any 32-bit system still using this type would suddenly believe it is 1901, which can crash software, break scheduling, invalidate certificates, or corrupt anything that reasons about time. It is the same shape of failure as the Year 2000 bug, just with a different arithmetic limit.

The fix is straightforward and already widespread: use a signed 64-bit integer for the time value. Sixty-four bits push the overflow roughly 292 billion years into the future, which is effectively forever. Modern 64-bit operating systems and current versions of major languages already do this. The remaining risk lives in older 32-bit embedded devices, long-lived firmware, and file formats or protocols that hard-coded a 32-bit time field. A related, milder issue is the Year 2106 problem, which affects systems using an unsigned 32-bit value โ€” those survive until 7 February 2106 before wrapping.

  • The overflow moment: 03:14:07 UTC, 19 January 2038.
  • Cause: signed 32-bit integer maxing out at 2,147,483,647 seconds.
  • Symptom: the clock jumps back to December 1901.
  • Fix: store time in a signed 64-bit integer (already the default on 64-bit systems).
  • Related: unsigned 32-bit storage overflows in 2106 instead.

Best practices for working with epoch time

Most timestamp bugs come from ambiguity โ€” about units, about zones, or about storage width. A few habits remove almost all of them. The theme throughout is: store one canonical form, and be explicit about which form it is.

Above all, decide on seconds or milliseconds once, document it in your API and schema, and never guess at read time. A field literally named createdAtMs or a comment stating the unit prevents the divide-by-1000 error that produces dates in 1970.

  • Store instants as UTC โ€” a Unix timestamp or an ISO 8601 string ending in Z โ€” and apply the display zone only in the UI.
  • Pin down seconds vs milliseconds explicitly and keep it consistent across your whole stack.
  • Use a 64-bit time type so 2038 is a non-issue; avoid new 32-bit time fields.
  • Prefer a named IANA zone (America/New_York) over a raw offset so daylight saving is handled correctly.
  • For human-facing text and file interchange, ISO 8601 (2026-07-11T14:00:00Z) is less error-prone than a bare number.
  • When debugging a mystery value, run it through a converter first โ€” it instantly reveals the unit and the resulting date so you are not doing arithmetic in your head.

Frequently Asked Questions

Always UTC. The number counts seconds from midnight UTC on 1 January 1970 and carries no time zone. Local time only appears when you format the number for display, so the same timestamp shows as different clock times in different places while remaining the same instant.

Count the digits. In the current era a seconds value is 10 digits (around 1.78 billion) and a milliseconds value is 13 digits (around 1.78 trillion). If a converted date comes out near 1970, you probably treated milliseconds as seconds; if it lands thousands of years in the future, you treated seconds as milliseconds. Multiply or divide by 1000 to fix it.

At 03:14:07 UTC on 19 January 2038, a Unix time stored in a signed 32-bit integer reaches its maximum of 2,147,483,647 and overflows one second later, wrapping to a negative value that represents 13 December 1901. Systems affected by this suddenly think it is 1901. The fix is storing the time in a signed 64-bit integer, which modern 64-bit systems already do.

Yes. Any date before the 1970 epoch is a negative number. For example, 1969-12-31 00:00 UTC is -86400 (one day of seconds before zero). Most modern libraries handle negative timestamps correctly, though some older or 32-bit systems mishandle them.

No. Unix time assumes every day is exactly 86400 seconds and ignores the 27 leap seconds inserted into UTC since 1972. That keeps it aligned to civil dates but means it is not a perfect count of physical seconds. This only matters for high-precision timing work; for everyday scheduling and logging it is irrelevant.

Those are the two classic tells. A date near January 1970 usually means a milliseconds value was interpreted as seconds (or a zero/near-zero value slipped in). A date in December 1901 is the signature of a 32-bit signed integer overflow โ€” the Year 2038 wrap, or a value that exceeded the 32-bit range.

Related Tools