Skip to main content

ISO 8601 Date and Time Format

How ISO 8601 strings are built, what Z and offsets mean, and the parsing traps that cause off-by-hours bugs.

Anatomy of an ISO 8601 string

The extended format writes the largest unit first: year, month, day, then a T, then hours, minutes and seconds, then a time zone designator. Example: 2026-09-20T12:30:00Z. Fractional seconds follow the seconds after a dot, as in 2026-09-20T12:30:00.250Z. A date can also stand alone, as in 2026-09-20.

Because the largest unit comes first and every field has a fixed width, strings in the same offset sort alphabetically in time order. That makes the format excellent for filenames, logs and database keys.

Z and UTC offsets

Z means UTC (zero offset). Otherwise the string ends with a signed offset from UTC: +02:00 is two hours ahead of UTC, -05:00 is five hours behind. So 2026-09-20T14:30:00+02:00 and 2026-09-20T12:30:00Z describe the same instant and have the same Unix timestamp.

An offset is not a time zone. It records the difference at that one moment, and says nothing about daylight saving changes later. If you need to schedule something in the future, store the IANA zone name too.

ISO 8601 vs RFC 3339

RFC 3339 is a stricter profile of ISO 8601 used by many internet protocols and APIs. It requires a full date and time with an offset or Z, and it allows a space instead of the T. Most JSON APIs that say they use ISO 8601 actually mean this profile.

Pitfalls when parsing

Strings without an offset are ambiguous. In JavaScript, new Date('2026-09-20') is parsed as midnight UTC, but new Date('2026-09-20T00:00:00') is parsed as midnight in the local time zone. The same source text can produce two different instants depending on whether a time is included.

The safest habit is to always include Z or an explicit offset when you write dates, and to reject or explicitly document how you treat strings that lack one.

new Date('2026-09-20').toISOString();          // 2026-09-20T00:00:00.000Z
new Date('2026-09-20T00:00:00').toISOString(); // depends on the local time zone