What is RFC 3339?
ISO 8601 is not one format but a family of them. 2026-10-17, 20261017, 2026-W42-6, 2026-290, 2026-10-17T14:30, T14:30:00,5 and 24:00 are all valid ISO 8601, and a parser that accepts all of them is large, slow and rarely complete. The standard itself is also paywalled, so most “ISO 8601 parsers” implement whatever subset their author guessed at.
RFC 3339, published on the Standards Track in July 2002, cuts that down to one unambiguous form for Internet protocols: a full calendar date, a full time, and an explicit offset from UTC. It describes itself as a profile of ISO 8601’s extended format, so every RFC 3339 timestamp is an ISO 8601 timestamp, but not the reverse.
When an API says a field is a timestamp, this is the format it should mean. JSON Schema’s date-time format and OpenAPI’s format: date-time are both defined by reference to it. The timestamps and formats guide covers the API design decisions around it; this page covers the grammar.
1. The grammar
The ABNF from Section 5.6:
date-fullyear = 4DIGITdate-month = 2DIGIT ; 01-12date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/yeartime-hour = 2DIGIT ; 00-23time-minute = 2DIGIT ; 00-59time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rulestime-secfrac = "." 1*DIGITtime-numoffset = ("+" / "-") time-hour ":" time-minutetime-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]full-date = date-fullyear "-" date-month "-" date-mdayfull-time = partial-time time-offset
date-time = full-date "T" full-timeThe examples from Section 5.8:
1985-04-12T23:20:50.52Z1996-12-19T16:39:57-08:001990-12-31T23:59:60Z1937-01-01T12:00:27.87+00:20Three things the grammar makes mandatory that ISO 8601 leaves optional: seconds, the separators (- and :), and the offset. A timestamp without an offset is not RFC 3339, and it is not a timestamp at all in any useful sense, because nobody can tell which instant it names.
2. RFC 3339 against ISO 8601
| Feature | ISO 8601 | RFC 3339 |
|---|---|---|
Basic format (20261017T143000Z) |
Allowed | Not allowed |
Week dates (2026-W42-6) and ordinal dates (2026-290) |
Allowed | Not in the grammar |
Reduced precision (2026-10, 14:30) |
Allowed | Seconds required |
Hour 24 |
Allowed (end of day) | 00 to 23 only |
Decimal comma (30,5) |
Allowed | Full stop only |
| Missing offset (local time) | Allowed | Not allowed |
Space instead of T |
Not allowed | Allowed “for readability” |
Lowercase t and z |
Not allowed | Allowed, but generators SHOULD use uppercase |
Offset -00:00 |
Not allowed | Allowed (see below) |
| Four-digit year | Expandable by agreement | Required |
The last three rows are where RFC 3339 is looser than ISO 8601, and they cause real interoperability bugs. Postgres and many log formats emit a space; strict ISO 8601 parsers reject it. Generate the strict intersection: uppercase T, uppercase Z, never a space, never -00:00. Be liberal only in what you parse, if you must.
3. Offsets, Z and -00:00
The offset is local time minus UTC: 16:39:57-08:00 is 00:39:57Z the next day. RFC 3339 originally drew a distinction:
| Offset | RFC 3339 (2002) meaning | RFC 9557 (2024) meaning |
|---|---|---|
Z |
UTC is the preferred reference point | UTC time known, local offset unknown |
+00:00 |
UTC is the preferred reference point | Unchanged: UTC is the preferred reference point |
-00:00 |
UTC time known, local offset unknown | Not formally deprecated, but Z is preferred, since ISO 8601 forbids -00:00 |
RFC 9557 moved the “unknown local offset” meaning onto Z because -00:00 is invalid ISO 8601 and many parsers reject it. For APIs the practical rule is unchanged: store and send UTC with Z. Nothing in an API client should be inferring the user’s local time from the offset on a timestamp.
4. Fractional and leap seconds
Fractional seconds take any number of digits after a full stop. The RFC notes that string comparison only sorts timestamps correctly when they all use the same offset and the same number of fractional digits, so pick a precision and pad to it. Three digits (milliseconds) or six (microseconds) are the usual choices; JavaScript’s Date holds milliseconds, so a client in that language will truncate anything finer.
A second value of 60 is legal at the end of June or December when a leap second is inserted (1990-12-31T23:59:60Z). Most platforms cannot represent it and either reject it or smear it. Do not generate one; accept it if you can, and do not crash if you see one.
5. Time zones: RFC 9557
An offset is not a time zone. -08:00 does not say whether the event is in Los Angeles or Vancouver, or what the offset will be next month after a daylight saving change. For future appointments and recurring events that matters.
RFC 9557 (Proposed Standard, April 2024) defines the Internet Extended Date/Time Format (IXDTF): an RFC 3339 timestamp followed by optional bracketed suffixes.
1996-12-19T16:39:57-08:00[America/Los_Angeles]1996-12-19T16:39:57-08:00[America/Los_Angeles][u-ca=hebrew]2022-07-08T00:14:07Z[!Europe/London]The bracket holds an IANA time zone name. A ! marks the suffix as critical: if the offset is inconsistent with the zone at that instant, the recipient MUST reject the value or resolve the conflict, whereas an elective suffix may simply be ignored. u-ca names a calendar for display. Using Z with a zone avoids inconsistency entirely, because the zone rules then derive the local time.
An IXDTF string is not a valid RFC 3339 string, so a field validated as JSON Schema date-time will reject it. If you need a zone, send it as a separate field ("timeZone": "America/Los_Angeles") unless every consumer is known to parse IXDTF.
6. Not the format for HTTP headers
HTTP fields do not use RFC 3339. Date, Last-Modified, Expires and Sunset use HTTP-date, whose preferred form is IMF-fixdate (Sun, 06 Nov 1994 08:49:37 GMT), and newer fields such as Deprecation use the Structured Fields Date type (@1688169599).
| Context | Format | Example |
|---|---|---|
| JSON bodies, query parameters | RFC 3339 | 2026-10-17T14:30:00Z |
| Older HTTP fields | HTTP-date (IMF-fixdate) | Sat, 17 Oct 2026 14:30:00 GMT |
| New HTTP fields | Structured Field Date | @1792247400 |
Do not put RFC 3339 in a header that is defined as HTTP-date, and do not put HTTP-date in a JSON body.
Best Practices
- Emit
YYYY-MM-DDTHH:MM:SS[.fff]Z, always UTC, always uppercase, always the same number of fractional digits. - Reject timestamps with no offset rather than guessing a zone.
- Document the precision you emit and the precision you accept.
- For a date with no time (a birthday, a due date), use RFC 3339
full-date(2026-10-17), JSON Schemaformat: date, not midnight UTC.