Unix Timestamps Explained: Seconds, Milliseconds and Time Zones
Drafted with AI assistance. Every command and code example was run and its output checked before publication. How guides are made
A Unix timestamp is the number of seconds that have passed since 00:00:00 UTC on 1 January 1970 (the Unix epoch), counted as if every day had exactly 86,400 seconds, so leap seconds are ignored. It names one instant, independent of time zone: 1700000000 is 22:13:20 UTC on 14 November 2023 everywhere in the world. Many systems count milliseconds instead (1700000000000), and mixing the two units is the most common timestamp bug.
What exactly is a Unix timestamp?
POSIX defines it in "Seconds Since the Epoch" as a formula over the UTC date and time: seconds, plus minutes × 60, plus hours × 3,600, plus days × 86,400, with leap years added by the Gregorian rules. Because the formula has no term for leap seconds, a Unix timestamp is not a true count of elapsed SI seconds. Twenty-seven leap seconds have been inserted into UTC since 1972, the last at the end of 2016, and none of them has a timestamp of its own; the difference between two timestamps that straddle one is a second short. For logging, APIs and scheduling this never matters, which is why the convention survives.
Negative values are dates before 1970: -86400 is 31 December 1969. POSIX leaves negative values undefined, but JavaScript, Python, PostgreSQL and GNU date all handle them.
Seconds, milliseconds or microseconds: how can I tell?
Count the digits. For any date between September 2001 and November 2286, a timestamp in seconds has 10 digits, and each finer unit adds three more:
| Unit | Digits today | Example (2023-11-14T22:13:20Z) | Typical sources |
|---|---|---|---|
| Seconds | 10 | 1700000000 | date +%s, PostgreSQL and MySQL functions, JWT exp/iat |
| Milliseconds | 13 | 1700000000000 | JavaScript Date.now(), Java System.currentTimeMillis() |
| Microseconds | 16 | 1700000000000000 | Some databases and tracing systems |
| Nanoseconds | 19 | 1700000000000000000 | Python time.time_ns(), Go UnixNano() |
The 10-digit range runs from 1000000000 (2001-09-09T01:46:40Z) to 9999999999 (2286-11-20T17:46:39Z). If you have to accept either unit in code, a threshold works for present-day data, but it is a heuristic; it is better to fix the unit in your API contract and name fields accordingly, such as created_at_ms.
// Treat values below 10^11 as seconds (valid for dates up to the year 5138)
const toMillis = t => (t < 1e11 ? t * 1000 : t);
new Date(toMillis(1700000000)).toISOString(); // '2023-11-14T22:13:20.000Z'
new Date(toMillis(1700000000000)).toISOString(); // '2023-11-14T22:13:20.000Z'
The Unix Timestamp Converter deliberately does not guess: you choose Seconds or Milliseconds, and it shows the instant in your local time, in UTC, in ISO 8601 and as a relative phrase.
How do I get the current Unix timestamp?
JavaScript
Date.now() returns milliseconds as a number. Divide and round down for seconds:
const ms = Date.now(); // e.g. 1791240918602
const seconds = Math.floor(Date.now() / 1000); // e.g. 1791240918
Python
time.time() returns seconds as a float with a fractional part; time.time_ns() returns an integer number of nanoseconds.
import time
int(time.time()) # seconds, e.g. 1791240918
time.time_ns() // 10**6 # milliseconds
Shell
date +%s # seconds since the epoch
SQL
-- PostgreSQL: extract returns numeric with a fractional part
SELECT floor(extract(epoch FROM now()))::bigint;
-- MySQL
SELECT UNIX_TIMESTAMP();
-- SQLite 3.38 or later
SELECT unixepoch();
In Java, System.currentTimeMillis() returns milliseconds; in Go, time.Now().Unix() returns seconds and UnixMilli() milliseconds, as documented in the time package.
How do I convert a timestamp to a date, and back?
Every example below uses the same instant, so you can compare the outputs. Run them with TZ=UTC or pass UTC explicitly, as shown, so that the result does not depend on the machine's settings.
JavaScript
new Date(1700000000 * 1000).toISOString(); // '2023-11-14T22:13:20.000Z'
Date.parse("2023-11-14T22:13:20Z") / 1000; // 1700000000
// Show it in a particular zone without changing the instant
new Date(1700000000000).toLocaleString("en-GB", { timeZone: "Asia/Tokyo" });
// '15/11/2023, 07:13:20'
Python
from datetime import datetime, timezone
datetime.fromtimestamp(1700000000, tz=timezone.utc).isoformat()
# '2023-11-14T22:13:20+00:00'
datetime.fromisoformat("2023-11-14T22:13:20Z").timestamp()
# 1700000000.0 (parsing a trailing Z needs Python 3.11 or later)
Always pass tz=. Without it, fromtimestamp() returns a naive local time, and utcfromtimestamp() has been deprecated since Python 3.12 in favour of timezone-aware objects (see the datetime documentation).
Shell (GNU date on Linux)
date -u -d @1700000000 +%Y-%m-%dT%H:%M:%SZ # 2023-11-14T22:13:20Z
date -u -d "2023-11-14T22:13:20Z" +%s # 1700000000
The BSD date on macOS uses different options: date -r 1700000000 formats a timestamp there.
PostgreSQL
SET TIME ZONE 'UTC';
SELECT to_timestamp(1700000000);
-- 2023-11-14 22:13:20+00
SELECT extract(epoch FROM timestamptz '2023-11-14 22:13:20+00')::bigint;
-- 1700000000
to_timestamp(double precision) returns timestamptz, which PostgreSQL displays in the session's TimeZone. For milliseconds, divide by 1000.0 first. See Date/Time Functions.
MySQL
SET time_zone = '+00:00';
SELECT FROM_UNIXTIME(1700000000); -- 2023-11-14 22:13:20
SELECT UNIX_TIMESTAMP('2023-11-14 22:13:20'); -- 1700000000
Both functions use the session time zone. With time_zone = '+09:00', the same FROM_UNIXTIME(1700000000) returns 2023-11-15 07:13:20, and UNIX_TIMESTAMP('2023-11-14 22:13:20') returns 1699967600, nine hours earlier, because the string is read as Tokyo time (MySQL 8.4 manual).
SQLite
SELECT datetime(1700000000, 'unixepoch'); -- 2023-11-14 22:13:20
SELECT unixepoch('2023-11-14 22:13:20'); -- 1700000000
SELECT strftime('%Y-%m-%dT%H:%M:%SZ', 1700000000, 'unixepoch');
-- 2023-11-14T22:13:20Z
SQLite treats date strings without an offset as UTC. The unixepoch() function needs SQLite 3.38 or later; older versions can use strftime('%s', ...) (SQLite date functions).
How do time zones affect Unix timestamps?
They do not. A timestamp counts from a fixed instant defined in UTC, so the same number means the same moment in London, New York and Tokyo. Time zones only enter when you turn the number into a calendar date and clock time, and that step needs a zone: either UTC or a named zone from the IANA time zone database, such as Europe/London. A fixed offset such as +01:00 is not enough for future local times, because offsets change with daylight saving and with local law.
The safe pattern is: store and transmit timestamps or UTC date-times, do arithmetic on them, and convert to the reader's zone only for display. When a user means a wall-clock time, such as "every day at 09:00 in Berlin", store the local time and the zone name together, and compute the timestamp when you need it; the cron syntax guide covers how schedulers deal with daylight saving.
ISO 8601 and RFC 3339: which string format should I use?
When a timestamp must be readable, use RFC 3339, a strict profile of ISO 8601 for internet protocols: 2023-11-14T22:13:20Z or, with an offset, 2023-11-15T07:13:20+09:00. Both denote the same instant as 1700000000. RFC 3339 requires a full date, a full time and an offset (Z means UTC), allows fractional seconds, and permits a space instead of the T for readability. ISO 8601 itself allows many more forms, such as week dates and basic format without separators, which many parsers do not accept.
Different tools write the UTC offset differently, though all are valid RFC 3339: JavaScript's toISOString() always gives milliseconds and Z; Python's isoformat() gives +00:00; PostgreSQL's text output uses a space and +00, which is not RFC 3339 and needs formatting before you put it in an API response.
What is the year 2038 problem?
A signed 32-bit integer holds at most 2,147,483,647. As a Unix timestamp that is 2038-01-19T03:14:07Z. One second later, a 32-bit counter overflows to −2,147,483,648, which reads as 1901-12-13T20:45:52Z. Modern operating systems and languages use 64-bit time values, but 32-bit fields survive in file formats, embedded firmware, older binaries and database schemas.
Check your storage as well as your code. MySQL's TIMESTAMP type has a documented range of '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC, whereas DATETIME reaches the year 9999 (MySQL 8.4 manual). An INT column holding seconds has the same 2038 limit; use BIGINT.
Common timestamp bugs and how to fix them
Milliseconds passed where seconds are expected (and vice versa)
Feed a 13-digit value to something expecting seconds and you land tens of thousands of years in the future: new Date(1700000000000 * 1000) is +055840-11-08T22:13:20.000Z, PostgreSQL's to_timestamp(1700000000000) gives 55840-11-08 22:13:20+00, MySQL's FROM_UNIXTIME returns NULL, and Python's fromtimestamp raises ValueError. The opposite mistake, seconds passed to new Date(), gives a date in January 1970: new Date(1700000000) is 1970-01-20T16:13:20.000Z. A date near 1970 or far beyond 2100 almost always means a unit mix-up.
Date-only strings in JavaScript are UTC; date-time strings are local
The ECMAScript date-time string format treats a date-only string as UTC but a date-time string without an offset as local time (MDN: Date.parse). With the machine set to America/New_York:
new Date("2026-10-05").toISOString(); // '2026-10-05T00:00:00.000Z' (UTC midnight)
new Date("2026-10-05T00:00").toISOString(); // '2026-10-05T04:00:00.000Z' (local midnight)
new Date("2026-10-05").toString();
// 'Sun Oct 04 2026 20:00:00 GMT-0400 (Eastern Daylight Time)'
So a birthday parsed from "2026-10-05" and shown with local getters appears as 4 October for users west of Greenwich. Either keep date-only values as strings, or parse and format them consistently in UTC (getUTCDate(), timeZone: "UTC"). Always include Z or an offset in date-time strings you send between systems.
Naive local times in Python
datetime(2023, 11, 14, 22, 13, 20).timestamp() has no time zone, so Python assumes the machine's local zone: it returns 1700000000.0 with TZ=UTC but 1700018000.0 in New York. Attach tzinfo=timezone.utc or a ZoneInfo before converting.
Losing precision in other places
Integer division truncates: keep milliseconds if ordering within a second matters. JavaScript numbers are exact only up to 253 − 1 (9,007,199,254,740,991). Present-day microsecond values still fit, but a 19-digit nanosecond timestamp parsed as a JSON number is silently rounded; send such values as strings or read them as BigInt.
Quick reference
| Task | Code |
|---|---|
| Now, seconds (JS) | Math.floor(Date.now() / 1000) |
| Seconds to ISO string (JS) | new Date(s * 1000).toISOString() |
| Now, seconds (Python) | int(time.time()) |
| Seconds to aware datetime (Python) | datetime.fromtimestamp(s, tz=timezone.utc) |
| Now / convert (shell) | date +%s / date -u -d @s |
| PostgreSQL | to_timestamp(s) / extract(epoch FROM ts) |
| MySQL | FROM_UNIXTIME(s) / UNIX_TIMESTAMP(dt) |
| SQLite | datetime(s, 'unixepoch') / unixepoch(dt) |
- Decide the unit (seconds or milliseconds) once and put it in field names and API docs.
- Store timestamps or UTC date-times in 64-bit columns; convert to local time only for display.
- Always include
Zor an offset in date-time strings. - Pass an explicit zone when converting in Python, SQL sessions and
toLocaleString. - Treat dates near 1970 or far in the future as a sign of a unit mix-up.
Frequently asked questions
Is a Unix timestamp always in UTC?
Yes. It counts seconds from 1970-01-01T00:00:00Z, so it has no time zone of its own and is the same number everywhere. A local time only appears when software formats it, using either UTC or a chosen zone.
Why does my converted date show 1970?
You passed seconds where milliseconds were expected, usually to JavaScript's new Date(). Multiply by 1,000 first: new Date(1700000000 * 1000). A date tens of thousands of years ahead is the opposite mistake.
Will Unix time stop working in 2038?
Only where it is stored in a signed 32-bit integer, which runs out at 2038-01-19T03:14:07Z. 64-bit values last far beyond any practical date, so the fix is to find and widen the remaining 32-bit fields, such as MySQL TIMESTAMP and INT columns.
What format do JWT exp and iat claims use?
Seconds since the epoch, which RFC 7519 calls NumericDate. Compare them with Math.floor(Date.now() / 1000), not with Date.now(). The JWT guide explains the time claims, and the JWT Decoder shows them in a token.
Should I store timestamps as integers or as a date-time type?
Either works if it is 64-bit and unambiguous. A native type such as PostgreSQL timestamptz gives you date functions and readable output; an integer is portable and compact. Avoid local-time columns without a zone, because they cannot be converted back to an instant reliably.