~/guides/-blog-unix-timestamps-explained-
guides · Time

Unix Timestamps Explained: Time, Epochs, and Timezones in Practice

What Unix time actually is, why it has an epoch problem in 2038, how to convert timestamps correctly across timezones, and the common bugs that trap developers.

last updated · June 14, 2026by @vultio

What a Unix timestamp actually represents

A Unix timestamp — also called Unix time, POSIX time, or epoch time — is a single integer that represents a point in time as the number of seconds elapsed since 00:00:00 UTC on Thursday, 1 January 1970. That specific moment is called the Unix epoch. Right now, as you read this, the Unix timestamp is somewhere around 1,750,000,000 — that is how many seconds have passed since the epoch.

The elegance of this design is that a timestamp is just a number. You can store it in a 32-bit or 64-bit integer, transmit it in a JSON payload, compare two timestamps with a single subtraction, sort events chronologically without parsing, and convert it to any calendar representation in any timezone without ambiguity. A timestamp is not "2026-06-14 in some timezone" — it is an exact, universal moment in time.

Timezones, local time, and calendar formatting are presentation concerns that happen after you have the timestamp. They are applied at the moment of display, not at the moment of storage or transmission. This separation — store as UTC epoch, display in local time — is the core principle behind working correctly with time in software.

Seconds vs milliseconds: the most common source of bugs

Unix time is defined in seconds. But many programming environments work inmilliseconds. JavaScript's Date.now() returns milliseconds. Java's System.currentTimeMillis() returns milliseconds. Python's time.time() returns seconds as a float. Redis'sTIME command returns seconds and microseconds separately.

The bug happens when a value in one unit is treated as another. If you store a JavaScript timestamp (milliseconds) and later try to convert it as if it were seconds, you get a date roughly 50 years in the future — the year 2527 or so. This is not a subtle bug: dates far in the future are obvious. But the inverse — treating seconds as milliseconds — gives you a date in January 1970, which is also obviously wrong. The challenge is that both directions look wrong in obvious ways, but in code that lacks assertions, the wrong value propagates silently until it hits a display layer.

How to identify which unit a timestamp is in

The easiest heuristic: timestamps in seconds have 10 digits right now (around 1,750,000,000). Timestamps in milliseconds have 13 digits (around 1,750,000,000,000). Timestamps in microseconds have 16 digits. If you see a 13-digit number in a payload labeled "timestamp", it's almost certainly milliseconds.

// JavaScript: always milliseconds
const msTimestamp = Date.now();             // e.g. 1750000000000
const sTimestamp = Math.floor(msTimestamp / 1000); // convert to seconds

// Python: seconds (as float with microseconds)
import time
ts_seconds = time.time()                   # e.g. 1750000000.123456
ts_ms = int(ts_seconds * 1000)             # convert to ms

// Converting a seconds timestamp to JavaScript Date
const date = new Date(sTimestamp * 1000);  // must multiply by 1000

Timezones: what timestamps protect you from

Timezone handling is where most date bugs live. The reason Unix timestamps are so widely used in systems programming is that they completely sidestep timezone complexity at storage time. A timestamp is always UTC. It does not care whether the server is in Paris, the database is in Singapore, or the user is in São Paulo. The same number means the same moment everywhere.

Problems arise when developers mix timestamps with local time strings. Consider a web form where a user picks a date and time in their local timezone. The frontend has a string like "2026-12-25 09:00" in the user's local timezone. If you send that string to the backend and store it as-is, you have lost the timezone. When you later read that string and convert it, the server's local timezone is used — which may be different from the user's timezone. The fix is to always convert local times to UTC before transmission, store UTC, and convert back to local time only for display.

Converting correctly in JavaScript

// Wrong: creating a Date from a local time string (assumes server timezone)
const wrong = new Date('2026-12-25 09:00');

// Right: always use ISO 8601 with UTC offset or Z
const right = new Date('2026-12-25T09:00:00Z');   // explicit UTC
const withOffset = new Date('2026-12-25T10:00:00+01:00'); // explicit CET

// Get Unix timestamp (seconds) from a Date object
const ts = Math.floor(right.getTime() / 1000);

// Convert Unix timestamp to ISO string for display
const iso = new Date(ts * 1000).toISOString(); // '2026-12-25T09:00:00.000Z'

// Display in user's local timezone (browser)
const local = new Date(ts * 1000).toLocaleString('it-IT', {
  timeZone: 'Europe/Rome'
}); // '25/12/2026, 10:00:00'

Daylight saving time: the silent edge case

Timestamps are immune to Daylight Saving Time (DST) because they are always UTC. But code that works with local times is not. If you do any arithmetic on local times — adding 24 hours to a local midnight to get the next day's midnight — you will get the wrong answer on DST transition days. On the day clocks spring forward, "tomorrow at midnight" is 23 hours away, not 24. On the day clocks fall back, it is 25 hours away.

The safe rule: do all date arithmetic in UTC or in epoch seconds. Convert to local time only for display. Libraries like date-fns, Temporal (the upcoming TC39 API),Luxon, or Python's datetime with timezone-aware objects handle DST correctly when given the right inputs. The plain Date object in JavaScript and naive datetimes in Python do not.

The Year 2038 problem

If Unix timestamps are stored in a signed 32-bit integer — which many older systems do — they can represent times up to 03:14:07 UTC on 19 January 2038. After that, a 32-bit signed integer overflows to a large negative number, which corresponds to 13 December 1901. This is the Unix equivalent of the Y2K problem.

Systems using 64-bit integers for Unix timestamps can store times up to approximately 292 billion years in the future, which is sufficient. Modern systems — Linux on 64-bit hardware, modern databases, and most programming languages — use 64-bit time values. But embedded systems, legacy databases, and applications that store timestamps as 32-bit integers in binary formats are still at risk. If you are working on or maintaining a system with long data retention requirements, verify that timestamps are stored in 64-bit types throughout the stack.

ISO 8601 vs Unix timestamps: when to use each

Unix timestamps are ideal for storage, comparison, arithmetic, and transmission in JSON APIs. They are compact, unambiguous, and fast to process. ISO 8601 strings (like2026-06-14T12:00:00Z) are better for human-readable logs, display, CSV exports, and contexts where a developer or user needs to read the value without a conversion step.

A common pattern that works well: store and transmit Unix timestamps in seconds as integers, include ISO 8601 strings in API responses alongside the timestamp for human readability and debugging convenience, and convert to local time strings only at the display layer. This gives you the best of both formats without the bugs that come from treating ISO strings as if they were timezone-aware timestamps.

Quick reference: common conversions

// Current timestamp in seconds
JavaScript:  Math.floor(Date.now() / 1000)
Python:      int(time.time())
Go:          time.Now().Unix()
PHP:         time()
SQL:         EXTRACT(EPOCH FROM NOW())  -- PostgreSQL
             UNIX_TIMESTAMP()           -- MySQL

// Timestamp to ISO string
JavaScript:  new Date(ts * 1000).toISOString()
Python:      datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()

// ISO string to timestamp
JavaScript:  Math.floor(new Date('2026-06-14T12:00:00Z').getTime() / 1000)
Python:      int(datetime.fromisoformat('2026-06-14T12:00:00+00:00').timestamp())

// Add 7 days to a timestamp (timezone-safe)
ts + (7 * 24 * 60 * 60)  // seconds — safe in all languages