Engineering · September 22, 2026

Unix timestamp conversion explained — epoch seconds, milliseconds, and ISO 8601

Unix timestamps look like a single integer until a unit mismatch turns today into 1970. Epoch seconds vs milliseconds, ISO 8601, time zones, and a conversion cheat sheet for the classic bugs.

Every system that stores a moment in time eventually asks the same question: is this number seconds or milliseconds? Unix timestamps look trivial — an integer counting the seconds since 1 January 1970 — and that is exactly why they get mishandled. A value that is wrong by a factor of 1000, a date that lands in 1970, a timezone bug that only appears for users in another hemisphere: they all come from the same handful of misunderstandings.

This post covers what a Unix timestamp actually measures, how seconds and milliseconds differ, how ISO 8601 fits in, and how to convert between formats without guessing.

What a Unix timestamp actually is

A Unix timestamp — also called epoch time — is the number of seconds elapsed since 00:00:00 UTC on 1 January 1970, the Unix epoch. It is a plain integer with no timezone attached: the same number describes the same instant everywhere on Earth.

Because it carries no timezone, the epoch is a natural interchange format for machines. APIs, logs, databases, and message queues store timestamps; people read dates. Conversion between the two is where the bugs live.

Note what a timestamp does not contain: a timezone, a locale, or a calendar. 1758537600 is an instant. Whether someone in Berlin or Bangkok sees 14:00 or 21:00 depends entirely on how you render it.

Seconds vs milliseconds: the most common bug

Two conventions coexist, and both are everywhere:

  • Seconds — 10 digits today, for example 1758537600. Traditional Unix, Go's Unix(), Java's getEpochSecond(), Redis, and most databases use this.
  • Milliseconds — 13 digits, for example 1758537600000. JavaScript's Date.now(), Java's getTime(), and most browser APIs use this.

Mix them up and the date jumps to 1970 or 2286. There is no universal rule for detecting the unit, so the reliable approach is to know your source's contract. If you are forced to guess, a 10-digit value is usually seconds and a 13-digit value is usually milliseconds — but guessing is the bug, not the fix.

Rule: store the unit next to the value. Use a column name (created_at_ms), an explicit API field, or a unit property. Anything beats a bare integer called time.

ISO 8601: the human-readable side

ISO 8601 strings are the counterpart to epoch numbers: readable, sortable when written in UTC, and unambiguous when an offset is included.

2026-09-22T14:00:00Z       # UTC, indicated by the trailing Z
2026-09-22T14:00:00+02:00  # the same instant, Berlin local time
2026-09-22T14:00:00.123Z   # milliseconds preserved

The trailing Z and the numeric offset are not decoration. Without one of them the string has no timezone, and your code has to guess — which is how "the same time" quietly becomes a three-hour bug.

Time zones and UTC

Store and compare in UTC, and convert to a local zone only at the edge, when you render for a person.

const now = Date.now();              // milliseconds since epoch
const seconds = Math.floor(now / 1000); // seconds since epoch
new Date(seconds * 1000).toISOString(); // "2026-09-22T14:00:00.000Z"

Two rules prevent most timezone problems:

  • Never compare one naive local time with another. Serialize both to instants first, then compare.
  • Keep the offset with the value when the user's zone matters (appointments, schedules, billing periods). A UTC instant plus an IANA zone ID such as Europe/Berlin beats a fixed offset, because fixed offsets change with daylight saving time.

Conversion cheat sheet

Convert JavaScript Python
milliseconds to a date string new Date(1758537600000).toISOString() datetime.fromtimestamp(1758537600, tz=timezone.utc)
date string to seconds Math.floor(Date.parse(s) / 1000) int(dt.replace(tzinfo=timezone.utc).timestamp())
seconds to milliseconds seconds * 1000 seconds * 1000
current time Date.now() (milliseconds) int(time.time()) (seconds)

Note that Date.now() returns milliseconds while time.time() returns seconds — a compact illustration that the unit is a per-language convention, not a global standard.

Mistakes to avoid

  • Scaling by 1000 "just in case". Decide from the contract, never from how large the number looks.
  • Storing local time without an offset. Daylight saving time will break the arithmetic eventually.
  • Parsing by splitting strings. Use the standard library parser: ISO 8601 has week dates, basic format, and fractional digits waiting to break hand-rolled code.
  • Formatting dates inside SQL. Keep the value numeric or UTC in the database and format in the application, where you control locale and timezone.
  • Using a full timestamp as a human-facing code. If people read it aloud they will drop digits. Round or hash it if you need a short reference.

Try it

Paste an epoch value or a date string into a timestamp converter to move between seconds, milliseconds, ISO 8601, and your local time. The conversion runs in your browser, so no timestamp — sensitive or otherwise — has to leave your machine just to be readable.