Skip to content
DevPenh

Unix Timestamps Explained: Seconds, Milliseconds and Time Zones

What epoch time is, how to tell seconds from milliseconds, and how to convert timestamps to UTC correctly in common languages.

Last reviewed

A Unix timestamp (also called epoch time or POSIX time) is the number of seconds that have elapsed since 1970-01-01 00:00:00 UTC, not counting leap seconds. It is a single number that identifies an instant, with no time zone attached, which makes it ideal for storing and comparing times across systems.

A timestamp has no time zone

1700000000 is the same instant everywhere on Earth. Time zones only appear when you convert it into a human-readable date:

Zone1700000000 displayed as
UTC2023-11-14 22:13:20
Asia/Phnom_Penh (UTC+7)2023-11-15 05:13:20
America/New_York (UTC−5 in November)2023-11-14 17:13:20

So the question "which time zone is this timestamp in?" has no answer. The real questions are which unit it uses, and which zone you want to display it in.

Seconds, milliseconds, microseconds, nanoseconds

The original Unix definition uses seconds, but many platforms use finer units. You can usually tell them apart by the number of digits for present-day dates:

  • 10 digits — seconds (1700000000)
  • 13 digits — milliseconds (1700000000000), used by JavaScript and Java
  • 16 digits — microseconds
  • 19 digits — nanoseconds, used by Go and many observability tools

Getting the unit wrong produces dates that are obviously off, which is a useful diagnostic:

  • A date in January 1970 usually means a seconds value was treated as milliseconds.
  • A date tens of thousands of years in the future (or an "Invalid Date") usually means a milliseconds value was treated as seconds.

The timestamp converter detects the unit from the digit count and lets you override it.

Converting correctly in common languages

JavaScript

// Current time
Date.now();                          // milliseconds
Math.floor(Date.now() / 1000);       // seconds

// Timestamp (seconds) to date
const d = new Date(1700000000 * 1000);
d.toISOString();                     // "2023-11-14T22:13:20.000Z"  (always UTC)
d.toLocaleString("en-US", { timeZone: "Asia/Phnom_Penh" }); // a specific zone

// Date string to timestamp: include an offset!
Date.parse("2023-11-14T22:13:20Z") / 1000;   // 1700000000

Be careful with ISO strings that lack an offset: new Date("2023-11-14") is treated as UTC, but new Date("2023-11-14T10:00") is treated as local time. Always include Z or an explicit offset when the zone matters.

Python

import time
from datetime import datetime, timezone

int(time.time())                                         # seconds

datetime.fromtimestamp(1700000000, tz=timezone.utc)
# datetime.datetime(2023, 11, 14, 22, 13, 20, tzinfo=datetime.timezone.utc)

datetime(2023, 11, 14, 22, 13, 20, tzinfo=timezone.utc).timestamp()
# 1700000000.0

Avoid datetime.utcfromtimestamp() and datetime.utcnow(): they return "naive" datetimes without a time zone, which are easy to misinterpret later, and both are deprecated since Python 3.12.

SQL

-- PostgreSQL
SELECT to_timestamp(1700000000);                -- timestamptz
SELECT extract(epoch FROM now())::bigint;       -- seconds

-- MySQL
SELECT FROM_UNIXTIME(1700000000);               -- in the session time zone
SELECT UNIX_TIMESTAMP();

Shell

date +%s                     # current time in seconds
date -u -d @1700000000       # GNU/Linux
date -u -r 1700000000        # macOS / BSD

Timestamps in JWTs and APIs

JWT claims such as exp, iat and nbf are defined in seconds. A classic bug is comparing exp with JavaScript's Date.now(), which is in milliseconds, so every token appears to have expired in 1970. Divide by 1000 first. The JWT decoder shows these claims as readable dates.

Leap seconds and the year 2038

Unix time assumes every day has exactly 86,400 seconds, so leap seconds are not counted; during a leap second systems either repeat a second or "smear" the extra time across a longer period. For most applications this is invisible.

The year-2038 problem is more practical. A signed 32-bit integer can hold up to 2147483647, which is 2038-01-19 03:14:07 UTC. Systems that still store seconds in 32-bit fields will overflow after that. Use 64-bit integers (or your database's native timestamp type) for anything that may need to represent dates beyond 2038 — including expiry dates set decades in advance.

Checklist

  • Store instants as UTC timestamps or timezone-aware types; convert to local time only for display.
  • Document the unit (seconds or milliseconds) in your API and field names, e.g. created_at_ms.
  • Always include an offset when serialising dates as strings (ISO 8601 with Z or +07:00).
  • Use 64-bit integers for timestamps.

Part of the API debugging series. Start with How to Debug API Responses: A Practical Workflow.