Two ways of writing a timestamp in UTC have meant the same thing for as long as most working programmers have been alive:
2026-09-11T01:02:03Z
2026-09-11T01:02:03+00:00
In April 2024 they stopped. RFC 9557 formally revised the specification that defines them, and under the current text those two strings make different assertions. No runtime we tested can represent the difference.
What the two documents say
RFC 3339, the specification everyone reads, sets out three spellings and their meanings. On the third:
"this can be represented with an offset of "-00:00". This differs semantically from an offset of "Z" or "+00:00", which imply that UTC is the preferred reference point"
So in 2002: Z and +00:00 together on one side, -00:00 — "the offset to local time is unknown" — on the other.
RFC 9557, which carries the header Updates: 3339, moves the line and describes itself as:
"aligning it with the actual practice of interpreting the offset Z to mean the same as -00:00"
Then it prints the replacement text for §4.3 in full:
"If the time in UTC is known, but the offset to local time is unknown, this can be represented with an offset of "Z". (The original version of this specification provided -00:00 for this purpose, which is not allowed by [ISO8601:2000] and therefore is less interoperable …) This differs semantically from an offset of +00:00, which implies that UTC is the preferred reference point for the specified time."
And it is explicit that the other half does not move:
"Note that the semantics of the local offset +00:00 is not updated; this retains the implication that UTC is the preferred reference point for the specified time."
⚠️ The effect is to move Z across to join -00:00, meaning the instant is known but the local offset is not. The offset +00:00 keeps its old meaning: UTC is the preferred reference point. The two spellings are no longer synonyms.
Why nobody noticed
Because the document that carries the news is not the document people fetch.
We downloaded RFC 3339's canonical text — 35,064 bytes — and searched it. The string 9557 appears zero times. So does Updated by. The controls in the same file are healthy: ISO 8601 appears 23 times and Z 22 times, so the fetch is real and the absences are real.
The information does exist. The metadata page for RFC 3339 on the IETF's own datatracker — 225,233 bytes — carries "Updated by (1)". But an RFC's canonical text is immutable by design, and the canonical text is what gets opened, bookmarked and pasted into a prompt.
Three standards, three meanings for one character
ECMAScript defines the character a third way. Its specification (956,219 bytes) says of the time-zone offset that Z is "(for UTC with no offset)", a definition matching neither RFC. Searching the same file: RFC 3339 appears zero times and RFC 9557 once, so the chapter is not unaware of the newer document; it simply never claimed conformance with the older one.
JSON Schema, meanwhile, binds its date-time format to the grammar in RFC 3339's section 5.6, which is the ABNF. The semantic distinction between the spellings lives in §4.3, so a purely syntactic validator never sees it. In JSON Schema's own text, 9557 appears zero times against six occurrences of RFC 3339.
What the runtimes actually do
We ran these rather than citing them.
Node 26.4.0, Date.parse of 2026-09-11T01:02:03…
Z → 1789088523000
+00:00 → 1789088523000
-00:00 → 1789088523000
+00:01 → 1789088463000 ← control: 60,000 ms apart
The three spellings collapse to one instant. The fourth line is the control that matters: the parser is reading offsets correctly, so the collapse is a deliberate equivalence rather than a broken parse.
PHP retains a trace and then discards it. getTimezone()->getName() returns Z for a Z input and +00:00 for both offset spellings, so the object does remember which character arrived. Both of its formatters then throw that away, in opposite directions:
input format('c') format('p')
Z 2026-09-11T01:02:03+00:00 2026-09-11T01:02:03Z
+00:00 2026-09-11T01:02:03+00:00 2026-09-11T01:02:03Z
-00:00 2026-09-11T01:02:03+00:00 2026-09-11T01:02:03Z
⚠️ A value that arrived as +00:00, asserting that UTC is the preferred reference point, is emitted as Z, asserting that the local offset is unknown. The same object makes opposite claims through two standard formatters, and format('c') is the one that produces DATE_RFC3339.
Python will not emit Z and will not read it either. Its isoformat() on a UTC-aware datetime returns +00:00, because the offset formatter has no branch for zero — a zero offset is not negative, so it takes the positive path. In the other direction, fromisoformat("2026-09-11T01:02:03Z") raises ValueError: Invalid isoformat string on this runtime, while the +00:00 spelling parses.
The sharpest version of the problem
Two standard-library constants, both named after RFC 3339, disagree about how to write the same instant.
| Constant | A UTC instant formats as |
|---|---|
Go's time.RFC3339 | 2026-09-11T01:02:03Z |
PHP's DATE_RFC3339 | 2026-09-11T01:02:03+00:00 |
Under the specification they are both named for, those two outputs now make different claims about whether the local offset is known.
What this site emits, since the question applies to us
Every article page here carries structured data with a published timestamp, and ours reads:
"datePublished":"2026-09-12T08:13:12+00:00"
That is the UTC-is-preferred form, and it is the right claim for us: these timestamps are stored in UTC and displayed in Singapore time, so the offset to local time is emphatically known. A site built on a runtime whose serialiser prefers Z — which is most of them — would publish the opposite assertion about the same kind of value, without anyone choosing it.
What this does not mean
The IETF has not done anything careless. Published RFCs are never edited, and that immutability is the reason a citation from 2002 still resolves to the same bytes today. The Updates: header and the datatracker are the intended path for exactly this, and RFC 9557 explicitly declines to deprecate anything — it ratifies what implementations were already doing, which moves the ecosystem toward less divergence rather than more.
The runtimes are not broken either. ECMAScript's Date predates both RFCs and never claimed conformance. JSON Schema binding to the ABNF is the correct choice for a syntactic validator. Python's formatter produces a string every parser accepts.
⚠️ This is not a bug report. No runtime contradicts its own documentation. The situation is narrower: a semantic distinction was added to a specification, but it lives only in prose that no tool reads.
What to do with it
Stop treating "normalise everything to Z" as a lossless clean-up step. Under the current text it is an assertion — that you know the instant and not the local offset. For a log line that is exactly right. For a scheduled meeting time it is wrong, and the fix is not a different offset but the mechanism RFC 9557 was actually written to introduce: a bracketed suffix naming the zone, as in 2026-09-11T01:02:03Z[Asia/Singapore].
Read "format": "date-time" in a JSON Schema as a syntax check and nothing more. It will not tell you which of the three spellings arrived, and the spellings no longer agree.
When you cite the specification in a design document, refer to RFC 3339 as updated by RFC 9557. That one extra clause points at the text in force, not merely the one your reader will find first.
Where this comes from, and what will date it
Both RFCs were read in full from their canonical text — RFC 3339 at 35,064 bytes and RFC 9557 at 39,113 bytes, the latter carrying Updates: 3339 in its own header. The ECMAScript and JSON Schema quotations come from those specifications directly, at 956,219 and 169,423 bytes. Every term count above was taken from the same fetched bytes as the quotation beside it, with a control term counted in the same file.
The Node, PHP and Python behaviour was executed here, on 26.4.0, 8.4.24 and 3.9.6 respectively, and the transcripts are what is printed above.
⚠️ Two limits. Go was read from its source, not run — we had no Go toolchain, so the constant and the comment beside it are quoted from src/time/format.go and the formatting behaviour is inferred from them rather than observed. And we make no claim at all about Java, because no runtime was available to test it.
One further caveat on provenance: the statement that ISO 8601 does not permit -00:00 is quoted from RFC 9557's own summary of it, not from the ISO text, which is paywalled. It is a secondary source and should be treated as one.
This finding is stable: an RFC's canonical text never changes. The situation would move only if a runtime decided to represent the distinction, which would be news. To check, re-run the six term counts and see whether any has left zero.