toolhq.io

All posts
6 min readby Jameel Haider

ISO 8601 explained: the only date format worth using

ISO 8601 is the international standard for writing dates and times as text: 2026-07-21T14:30:00Z. Largest unit first, fixed widths, timezone explicit. It is what JSON APIs, logs, databases, and filenames should use, for one elegant reason above all: ISO 8601 strings sort correctly as plain text. Alphabetical order is chronological order.

If you are converting between these strings and Unix timestamps right now, the Timestamp Converter does both directions live in your browser.

The anatomy of an ISO 8601 timestamp

2026-07-21T14:30:00.123+02:00
└──┬───┘ └───┬──────┘└──┬─┘
  date      time      offset
  • Date: YYYY-MM-DD, always four digit year, always zero padded. 2026-07-05, never 2026-7-5.
  • T: a literal separator between date and time. A space is common in the wild and allowed by the related RFC 3339 profile, but T is the safe interchange choice.
  • Time: HH:MM:SS, 24 hour clock, optional fractional seconds after a dot.
  • Offset: the timezone information, and the part people get wrong most.

Precision is flexible by truncating from the right: 2026-07-21 alone is a valid date, 2026-07 a month, 14:30 a time without seconds.

What do the Z and the offsets mean?

SuffixMeaning
ZUTC. "Zulu time" in aviation phonetics
+02:00Two hours ahead of UTC
-05:00Five hours behind UTC
(none)Local time, unspecified. Ambiguous. Avoid

14:30:00Z and 16:30:00+02:00 are the same instant written two ways. The offset does not name a timezone: +02:00 could be Berlin in summer or Cairo year round. If you need to know the place, for recurring events or "9am local" semantics, you need an IANA zone name like Europe/Berlin alongside, because offsets change with daylight saving and the string cannot tell you that.

A timestamp with no offset at all is the source of endless bugs: two systems each assume their own local time and disagree silently. The rule that prevents the whole class: store and transmit UTC, convert at the display edge.

Why do ISO 8601 dates sort correctly as text?

Fixed widths and largest first means character by character comparison agrees with time:

2026-07-09T08:00:00Z
2026-07-10T07:59:59Z
2026-07-10T08:00:00Z

No parsing needed. Database indexes on string columns, sort on log files, and alphabetically listed filenames all just work. Compare 07/09/2026 style dates, which sort by month first and interleave years hopelessly.

This only holds when every producer pads correctly and, strictly, when everything shares one offset, which is one more argument for UTC everywhere.

The formats it replaces, and the ambiguity problem

04/07/2026 is July 4th in the US and April 7th almost everywhere else. Every application that accepts slash dates from international users eventually corrupts data with this. YYYY-MM-DD has exactly one reading in every country. That is the entire pitch, and it is enough.

The related standard you will see cited in API docs is RFC 3339, essentially the internet profile of ISO 8601: it requires a full date and time with an offset, and drops the exotic corners of the ISO spec (week dates like 2026-W30-2, ordinal dates like 2026-202, durations like P3DT4H). If your API "speaks ISO 8601", it almost always means RFC 3339. Emit that shape and you are compatible with both.

ISO 8601 vs Unix timestamps

The two encodings solve different problems and pair well:

ISO 8601Unix timestamp
Example2026-07-21T14:30:00Z1784989800
Human readableYesNo
Compact, math friendlyNoYes
Timezone infoExplicitAlways UTC by definition
Typical homeAPIs, logs, JSONDatabases, code, JWT claims

A reasonable convention: integers internally, ISO 8601 at every boundary a human or a foreign system might see. The Unix side has its own subtleties, seconds vs milliseconds above all, covered in Unix timestamps and epoch time.

The JavaScript pitfalls

JavaScript parses ISO 8601 natively, with two traps worth knowing cold:

Date only strings parse as UTC, datetime strings without offset parse as local.

new Date('2026-07-21')          // 2026-07-21T00:00:00 UTC
new Date('2026-07-21T00:00')    // midnight in YOUR timezone

West of Greenwich, the first line displays as July 20th locally. This single inconsistency, mandated by the spec for backward compatibility, has produced a generation of "my date is off by one day" bugs. The fix: always include an explicit offset, or construct dates from numeric parts.

toISOString() always emits UTC. new Date().toISOString() gives 2026-07-21T12:30:00.000Z regardless of the machine's zone, which is exactly what you want for logs and APIs, and occasionally surprising when you wanted local wall time.

Python users have a mirror image trap: before 3.11, datetime.fromisoformat() rejected the trailing Z, and older code is full of .replace('Z', '+00:00') for that reason.

Practical rules

  • Store UTC. Convert to local time only when rendering for a human.
  • Always include the offset. A timestamp without one is a bug waiting for a second timezone.
  • Use T, zero padding, and four digit years exactly as specified. The format's value is its rigidity.
  • Filenames and log prefixes in YYYY-MM-DD sort themselves forever.

To convert any ISO 8601 string to a Unix timestamp and back, or to see the current moment in both forms, the Timestamp Converter runs entirely client side, nothing sent anywhere.