Every API that carries a time has to choose a representation. The two common choices are a Unix timestamp, a count of seconds or milliseconds since 1970-01-01T00:00:00Z, and an ISO 8601 string such as 2026-10-25T09:30:00Z. Both can describe an instant exactly. They differ in what else they carry, how they sort, and how easy they are to misread.
If you have a value in one format and need the other, convert between Unix time and ISO 8601 in your browser. The converter works in both directions and lists ISO 8601, Unix seconds and Unix milliseconds for UTC, your local zone and one extra zone you choose.
One instant, several spellings
Here is 25 October 2026 at 09:30 UTC in the forms you are likely to meet:
1792920600 Unix seconds
1792920600000 Unix milliseconds
2026-10-25T09:30:00Z ISO 8601 and RFC 3339, UTC
2026-10-25T09:30:00.000Z JavaScript toISOString()
2026-10-25T09:30:00+00:00 Python isoformat() on a UTC datetime
2026-10-25T15:00:00+05:30 the same instant in Asia/Kolkata
2026-10-25T05:30:00-04:00 the same instant in America/New_York
2026-10-25T10:30:00+01:00 the same instant in Europe/Berlin
All eight identify one moment. Look at the Berlin line: Europe left summer time at 01:00 UTC that morning, so a few hours earlier the same city was at +02:00. An offset is a fact about one instant, not about a place.
What each format carries
A Unix timestamp is just a number. It says nothing about its unit or about where the event happened, and it ignores leap seconds: every Unix day is exactly 86,400 seconds. That minimalism is the point. The value is identical on every machine, and subtracting two timestamps gives a duration with no parsing at all.
An ISO 8601 string carries a calendar date, a time of day, an optional fraction and, if the producer includes one, an offset from UTC. With an offset, a reader recovers both the instant and the wall-clock time the producer saw (15:00 in Kolkata). It still doesn't carry the time zone: +01:00 could be Berlin in winter, Lagos all year, or several other places.
Side by side
| Unix timestamp | ISO 8601 string | |
|---|---|---|
| Readable in a log | No, needs converting | Yes |
| Self-describing | No, seconds or milliseconds is a guess | Yes |
| Keeps local time | No | Yes, when an offset is included |
| Size in JSON | 10 or 13 digits | 20 to 29 characters, plus quotes |
| Sorting | Numeric sort is always right | String sort is right only with one fixed format and offset |
| Arithmetic | Subtract directly | Parse first |
| Precision | Fixed by the unit | Any number of fractional digits |
The sorting trap
ISO 8601 is often called "sortable as a string". That holds only when every value uses the same offset and the same precision. Mix them and string order can be exactly backwards:
["2026-10-25T09:30:00.5Z",
"2026-10-25T09:30:00Z",
"2026-10-25T10:00:00+01:00"].sort()
// returns the same order, which is wrong
// the same values as Unix milliseconds:
// 1792920600500, 1792920600000, 1792918800000
// chronological order is the reverse
The fraction breaks the first pair, because . sorts before Z. The offset breaks the last one, because 10:00+01:00 is 09:00 UTC. If you sort or index ISO strings, normalise them first: always UTC, always Z, always the same number of fractional digits. Better still, sort on a native timestamp column in the database.
Where RFC 3339 fits
ISO 8601 is a large standard. It allows week dates (2026-W43-7), ordinal dates (2026-298), the compact basic format (20261025T093000Z), reduced precision (2026-10), local times with no offset, and durations. Few parsers accept all of it.
RFC 3339 is a profile of ISO 8601 for internet protocols. It requires a full date, a full time and an offset (Z or ±hh:mm), and leaves the rest out. It also adds a few details of its own: lowercase t and z are allowed, applications may use a space instead of T for readability, -00:00 means "the UTC time is known but the local offset isn't", and a seconds value of 60 is allowed for leap seconds. When JSON Schema or OpenAPI says date-time, it means RFC 3339.
In practice, emit strings like 2026-10-25T09:30:00Z, which are valid under both standards, and be a little more lenient when you parse values typed by people.
Which one to choose
- Public JSON APIs and logs: RFC 3339 in UTC. People can read it in a log line, it's unambiguous, and every language can parse it.
- When the local time matters (a receipt, a check-in time): RFC 3339 with the producer's offset, plus the IANA zone in a separate field if you will compute future times from it.
- Specs that require numbers: the JWT
exp,iatandnbfclaims are seconds since the epoch by definition (RFC 7519). Follow the spec; the guide to JWT exp and iat claims covers the details. - High-volume internal data such as metrics and events: integer epoch values are smaller and cheaper to compare. Put the unit in the field name (
ts_ms). - Dates without a time (birthdays, due dates): a plain
YYYY-MM-DD. Converting them to a timestamp invents a time and a zone, and they shift by a day when displayed elsewhere. - Database columns: neither. Use the native type, such as
timestamptzin PostgreSQL, and convert at the API boundary.
Converting in code
// JavaScript
new Date(1792920600 * 1000).toISOString() // "2026-10-25T09:30:00.000Z"
Math.floor(Date.parse("2026-10-25T09:30:00Z") / 1000) // 1792920600
# Python 3.11+
from datetime import datetime, timezone
datetime.fromtimestamp(1792920600, tz=timezone.utc).isoformat()
# '2026-10-25T09:30:00+00:00'
int(datetime.fromisoformat("2026-10-25T09:30:00Z").timestamp())
# 1792920600
# Shell
date -u -d @1792920600 +%Y-%m-%dT%H:%M:%SZ # GNU date (Linux)
date -u -r 1792920600 +%Y-%m-%dT%H:%M:%SZ # BSD date (macOS)
Three details trip people up in Python. It writes +00:00 rather than Z; both are valid, but a string comparison against Z values fails. fromisoformat accepts the Z suffix only from Python 3.11; older versions raise ValueError. And without tz=timezone.utc, fromtimestamp returns the machine's local time, which is the start of many of the bugs in debugging timezone errors in APIs.
Next steps
Paste either format into the epoch converter to check a value before it goes into a fixture or a bug report. If you settle on Unix time, read seconds vs milliseconds so the unit never becomes a guess.