Skip to main content

Date and Time on the Internet (RFC 3339)

A reference for RFC 3339, the profile of ISO 8601 that JSON APIs use for timestamps: the date-time grammar, offsets, Z and -00:00, fractional and leap seconds, the RFC 9557 time zone suffix, and how it differs from ISO 8601 and HTTP-date.

Current versionRFC 3339 (updated by RFC 9557)
Specificationrfc-editor.org
Updated2026-10-17

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 = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
time-secfrac = "." 1*DIGIT
time-numoffset = ("+" / "-") time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
date-time = full-date "T" full-time

The examples from Section 5.8:

1985-04-12T23:20:50.52Z
1996-12-19T16:39:57-08:00
1990-12-31T23:59:60Z
1937-01-01T12:00:27.87+00:20

Three 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 Schema format: date, not midnight UTC.
On this page (8 sections)