RFC 3339 vs ISO 8601
· formats
Almost the same, but not quite a subset in either direction. If you are writing an API, target the overlap.
ISO 8601 is a large, permissive international standard covering dates, times, durations, intervals and recurrence. RFC 3339 is a short internet specification that profiles the parts relevant to timestamps and pins down what ISO leaves optional.
For interchange, RFC 3339 is almost always the right target.
What RFC 3339 requires
2024-03-10T06:00:00Z
2024-03-10T11:30:00+05:30
2024-03-10T06:00:00.123456Z
Mandatory: a four-digit year, two-digit month and day, T or a space separator, hours, minutes, seconds, and an offset or Z. Fractional seconds are optional with unlimited digits.
The important constraint: the offset is not optional. ISO 8601 permits a naked local time with no zone information; RFC 3339 does not. This single rule eliminates the most common source of timestamp ambiguity.
Where they differ
| ISO 8601 | RFC 3339 | |
|---|---|---|
| Offset required | No | Yes |
Space instead of T |
No | Yes (permitted) |
Lowercase t / z |
No | Yes (permitted) |
-00:00 (unknown offset) |
Prohibited | Allowed, with meaning |
24:00:00 |
Allowed | Prohibited |
Week dates (2024-W10-7) |
Allowed | Not covered |
Ordinal dates (2024-070) |
Allowed | Not covered |
Basic format (20240310T060000Z) |
Allowed | Not covered |
Durations (P1Y2M) |
Allowed | Not covered |
Leap second :60 |
Allowed | Allowed |
Neither is a strict subset of the other. RFC 3339 permits a space separator and -00:00, both of which ISO 8601 forbids; ISO 8601 permits week dates and 24:00:00, which RFC 3339 does not cover or forbids.
The -00:00 case
RFC 3339 gives -00:00 a distinct meaning: the instant is known but the local offset is not. +00:00 asserts UTC; -00:00 asserts “UTC, and we do not know what the local clock read”.
The distinction is rarely used and inconsistently implemented. Accept it on input, do not emit it.
The safe intersection
For anything crossing a system boundary, emit:
YYYY-MM-DDTHH:MM:SSZ
Uppercase T, uppercase Z, UTC, no fractional seconds unless you need them. This is valid under both standards and parses everywhere without configuration.
If you need sub-second precision, three or six digits are the widely supported choices:
2024-03-10T06:00:00.123Z
2024-03-10T06:00:00.123456Z
Note that some parsers accept only three. Nanosecond precision (nine digits) is legal but poorly supported outside Go and Rust.
What other specifications use
- JSON Schema
format: "date-time"— RFC 3339. - OpenAPI
format: date-time— RFC 3339. - HTTP
Dateheaders — neither; RFC 7231 uses its own format with a literalGMT. - Email
Dateheaders — RFC 5322, theSun, 10 Mar 2024 06:00:00 +0000form. - XML Schema
xs:dateTime— ISO 8601, with its own restrictions. - JWT
exp/iat/nbf— not a string at all; Unix seconds as a number.
So an API that speaks JSON should emit RFC 3339, but the HTTP headers wrapping it use a different format entirely, and any JWT inside uses integers. All three in one response is normal.
Language support
new Date().toISOString(); // 2024-03-10T06:00:00.000Z — RFC 3339 valid
from datetime import datetime, timezone
datetime.now(timezone.utc).isoformat() # '+00:00' rather than 'Z'
# Python's fromisoformat only accepted a trailing 'Z' from 3.11 onwards
time.Now().UTC().Format(time.RFC3339) // Go names it explicitly
Python’s default output uses +00:00 where JavaScript uses Z. Both are valid RFC 3339 and represent the same instant, but string comparison between them fails — never compare timestamps as strings.
Practical rules
- Emit
YYYY-MM-DDTHH:MM:SSZunless you have a specific reason not to. - Always include an offset. This is the rule that matters most.
- Parse leniently — accept a space separator, lowercase
t/z, and any number of fractional digits. - Compare instants after parsing, never as strings.
- Do not use week dates, ordinal dates or
24:00:00in interchange.
See ISO 8601 explained for the fuller treatment of the underlying standard. The converter shows the RFC 3339 form of any timestamp in the ISO 8601 row.