Convert a Timestamp to an ISO Date: Seconds, Milliseconds and Time Zones

Converting an epoch timestamp to an ISO date takes one line of code once you know two things: the unit of the number and the time zone the output should show. The failures are silent. A seconds value treated as milliseconds lands in January 1970, local-time formatting shifts the clock by your offset, and dropping an offset turns an instant into a wall time nobody can place. Identify the unit, build the instant in UTC, and emit ISO 8601 with `Z` or an explicit offset.

Know what an ISO timestamp represents

A Unix timestamp counts elapsed seconds from 1970-01-01T00:00:00Z; common APIs also use milliseconds, microseconds or nanoseconds. It identifies an instant independent of location. An ISO 8601 date-time expresses that instant as calendar fields plus a zone designator.

1704067200       seconds
1704067200000    milliseconds

2024-01-01T00:00:00Z
2024-01-01T01:00:00+01:00
# Both ISO strings identify the same instant.

Z means UTC, sometimes called Zulu time. An offset such as +01:00 states that the displayed local clock is one hour ahead of UTC. A string without either, such as 2024-01-01T00:00:00, is only a local wall time; consumers must guess which zone applies.

ISO 8601 covers dates, times, durations and several representations. For API interchange, the familiar extended calendar form compatible with RFC 3339—date, T, time and a numeric offset—is the least ambiguous choice.

Seconds versus milliseconds is the first check

Before any formatting, the number has to be in the unit your date API expects: milliseconds for JavaScript's Date, seconds for Python's datetime.fromtimestamp() and PHP's @ syntax. Current values are about ten digits in seconds and thirteen in milliseconds; sixteen-digit microseconds and nineteen-digit nanoseconds, common in tracing systems and some databases, need dividing down first.

Getting the unit wrong rarely throws. Seconds read as milliseconds give a plausible date in January 1970. Milliseconds read as seconds give the year 55969 in JavaScript and PHP, while Python refuses with ValueError: year must be in 1..9999. Sanity-check the result against the domain: a newly created order should not predate the web.

The unit also limits the precision the ISO string can honestly claim. A seconds value formatted with toISOString() always ends in .000Z; those zeros are padding, not measured milliseconds. If consumers might read meaning into the fraction, format with the precision you actually have.

Convert to ISO 8601 in common languages

JavaScript's Date stores milliseconds since the epoch and toISOString() always emits UTC with Z. Python can construct an aware UTC datetime directly from seconds. PHP's @timestamp syntax also starts in UTC, after which an explicit formatter keeps the offset visible.

const seconds = 1704067200;
const iso = new Date(seconds * 1000).toISOString();
console.log(iso); // 2024-01-01T00:00:00.000Z
from datetime import datetime, timezone

seconds = 1704067200
iso = datetime.fromtimestamp(seconds, tz=timezone.utc).isoformat()
print(iso)  # 2024-01-01T00:00:00+00:00
$seconds = 1704067200;
$date = new DateTimeImmutable('@' . $seconds);
echo $date->format('Y-m-d\TH:i:s.v\Z');
// 2024-01-01T00:00:00.000Z

The differing fractional parts do not represent different instants. JavaScript prints milliseconds, Python omits a zero fraction by default, and the PHP format above requests milliseconds explicitly. Choose a stable precision for your API so string comparisons and cache keys do not change between producers.

Preserve UTC and offsets

A timestamp has no time zone to preserve; it is already an absolute instant. The zone matters only when rendering calendar fields. Convert to UTC for storage and interchange, and convert to an IANA zone such as Europe/Paris only for display or a business rule tied to local civil time.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

instant = datetime.fromtimestamp(1711848600, tz=timezone.utc)
print(instant.isoformat())
print(instant.astimezone(ZoneInfo('Europe/Paris')).isoformat())
# Offset is calculated for that date, including daylight-saving rules.

Do not add a fixed number of hours to obtain local time. Daylight-saving transitions and historical rules mean a location is not a permanent offset. An offset records one relationship at one instant; an IANA zone supplies the rules needed to schedule future local times.

If input arrives as an ISO timestamp with an offset, parse it as aware time and convert the resulting instant. Removing +02:00 before parsing changes the instant by two hours rather than merely changing its display.

Fractions, negative timestamps and numeric precision

Milliseconds often arrive as an integer, while seconds may include a fraction such as 1704067200.125. Split or convert with care: binary floating-point cannot represent every decimal fraction exactly. For logs this is usually harmless; for ordering financial events or generating signatures, retain the integer unit supplied by the producer.

const milliseconds = 1704067200123n;
const wholeSeconds = milliseconds / 1000n;
const remainderMs = milliseconds % 1000n;
// 1704067200 seconds and 123 milliseconds, with no float rounding

JavaScript numbers exactly represent integer millisecond timestamps for ordinary application dates, but nanosecond epoch values already exceed the safe-integer limit. Parse those as BigInt or text and use a library whose precision matches the input.

Negative timestamps represent instants before the epoch. Some platform date APIs support them and some legacy environments do not. Test the actual runtime if historical dates matter, especially on systems whose native time representation has a narrower range.

Parsing ISO back into a timestamp

Round trips are safe only when the ISO input includes a zone. JavaScript parses a trailing Z or explicit offset as an instant; Date.parse() returns milliseconds. Python's aware datetime can return fractional seconds. Never append Z to a local timestamp unless you know its fields already express UTC.

const iso = '2024-01-01T01:00:00+01:00';
const milliseconds = Date.parse(iso);
console.log(milliseconds); // 1704067200000
console.log(new Date(milliseconds).toISOString());
// 2024-01-01T00:00:00.000Z

Date-only input introduces another semantic question. 2024-01-01 describes a calendar date, not necessarily midnight UTC. If the business value is a birthday or billing day, keep it as a date rather than manufacturing an instant that may display as the previous day in another zone. The same applies to arithmetic on such values: the number of days between two calendar dates, or a person's age, should be counted in calendar days, which is what a date difference calculator does, not by dividing a difference in seconds by 86,400.

Reject impossible or partially parsed dates. A lenient parser that turns an invalid day into the following month produces silent corruption. Validate the entire input against the format your API promises.

Conversion checklist

Confirm whether the input is seconds, milliseconds, microseconds or nanoseconds. Parse it without losing integer precision. Construct an instant in UTC, then format it with Z or an explicit numeric offset. Finally, sanity-check the year and round-trip the output when correctness is important.

input:  1704067200000
unit:   milliseconds (from API contract)
instant: epoch + 1704067200 seconds
output: 2024-01-01T00:00:00.000Z
check:  Date.parse(output) === input

At API boundaries, document both type and unit. integer, Unix milliseconds is actionable; timestamp is not. If clients do not need arithmetic, an ISO string is often clearer and avoids unit guessing, although you should still specify whether fractional seconds are required.

For user-facing display, keep the ISO UTC value as the source and format it in the user's chosen zone at the edge. Storing preformatted local strings loses the information required to handle daylight-saving changes or move the record between locales.

Frequently asked questions

How do I convert a Unix timestamp to an ISO date?

Determine the unit first, construct the corresponding instant, and format it in UTC. In JavaScript, multiply epoch seconds by 1,000 before passing them to `new Date()`, then call `toISOString()`.

Is an ISO timestamp in seconds or milliseconds?

An ISO timestamp is a formatted date-time string, so it has neither epoch unit. Unix timestamps are commonly seconds or milliseconds; the API contract must say which. Fractional seconds in an ISO string represent precision, not an epoch unit.

What is the ISO 8601 format for a date and time?

The form APIs use is `YYYY-MM-DDTHH:mm:ss`, optionally followed by a fraction of a second, then `Z` for UTC or an offset such as `+02:00`, for example `2024-01-01T00:00:00.000Z`. RFC 3339 is the internet profile of ISO 8601 and makes the offset mandatory.

What does Z mean in an ISO date?

`Z` means the time is expressed in UTC, with an offset of zero. It makes the string an unambiguous instant; without `Z` or another offset, the value is a local wall time whose zone must be supplied separately.

Should timestamps be stored in UTC?

Store event instants in UTC or as an epoch value and convert them for display. Future appointments tied to a local clock also need the IANA zone, because a fixed offset cannot encode future daylight-saving rules.

Can JavaScript safely handle nanosecond timestamps?

Not as ordinary numbers: current epoch nanoseconds exceed JavaScript's safe-integer range. Keep them as `BigInt` or decimal text and use a conversion library that preserves nanosecond precision.