Skip to content

Guide

Epoch, ISO 8601 or RFC 3339: which date format for an API?

Three formats for the same instant, and only one of them is unambiguous without reading the docs.

By Published

Every API that touches dates eventually has to answer one question: what does a timestamp field actually look like on the wire? There are really three candidates in common use — a raw Unix epoch integer, an ISO 8601string, and its stricter cousin RFC 3339. They all describe the same instant in time, but they fail differently when something downstream gets it wrong, and picking the right one up front avoids a whole category of “why is this date off by a few hours” bug reports later.

What each format actually is

A Unix epoch timestampis an integer: the number of seconds (or milliseconds, or less commonly microseconds or nanoseconds) since 1970-01-01T00:00:00Z, per POSIX’s definition of time_t. It carries no timezone — it’s always relative to UTC by construction — but it also carries no unit marker, which is where most of its real-world trouble comes from.

ISO 8601is the international standard for representing dates and times as text, and it is deliberately broad: it permits calendar dates, week-numbering dates, ordinal dates, durations, intervals, reduced precision (just a year, or a year and month), and time values with or without a UTC offset. That flexibility is exactly what makes “ISO 8601 compliant” a weaker guarantee than it sounds — two systems can both be technically compliant and still produce strings the other can’t parse.

RFC 3339is the IETF’s internet-facing profile of ISO 8601: it keeps exactly one shape (YYYY-MM-DDTHH:MM:SS[.ffffff](Z|±HH:MM)) and drops everything ISO 8601 allows that would make automated parsing ambiguous — no week dates, no reduced precision, and an offset is mandatory, never implied. When people say “ISO 8601 timestamp” in an API context, what they almost always mean, and what their code almost always expects, is RFC 3339.

FormatExampleSelf-describing?Ambiguity risk
Epoch (seconds)1700000000NoHigh — unit not marked
Epoch (milliseconds)1700000000000NoHigh — unit not marked
ISO 8601 (general)2023-11-14T22:13:20PartlyMedium — offset may be omitted
RFC 33392023-11-14T22:13:20ZYesLow — offset mandatory, one shape

Where each one actually breaks

Epoch integers fail silently, which is worse than failing loudly. 1700000000is a perfectly valid number whether it means seconds (14 November 2023) or milliseconds (20 January 1970) — a wrong unit assumption doesn’t throw an error, it just produces a date that’s wrong by orders of magnitude and looks plausible until someone notices. This is the single most common timestamp bug in systems that pass epoch values between services written in different languages, each with its own default unit.

Plain ISO 8601 strings fail on the offset. A string like 2023-11-14T22:13:20 with no Z or ±hh:mmis valid ISO 8601 but genuinely ambiguous — is it UTC, local server time, or the originating user’s local time? Every consumer has to guess or consult out-of-band documentation, and different consumers sometimes guess differently.

RFC 3339 mostly avoids both failure modes by construction, which is why it’s the default recommendation in API style guides — Google’s AIP-142, for instance, mandates it for timestamp fields in new APIs. Its remaining edge case is fractional-second precision: the spec allows any number of digits after the decimal point, so a producer emitting nanosecond precision and a consumer that truncates to milliseconds can still disagree on the exact instant, just by a much smaller margin than a unit mismatch.

Picking a format for a new field

For a public or cross-service API, default to an RFC 3339 string. It’s readable in raw logs and debuggers without decoding, it’s unambiguous about both timezone and precision, and virtually every mainstream language’s date library parses and emits it natively. The one cost is a few extra bytes per value compared to an integer — irrelevant for almost every API payload.

Reach for an epoch integer when the field is high-volume telemetry, a metrics timestamp, or an internal ordering/sort key where every producer and consumer is under your control and already agrees on the unit. Even then, document the unit explicitly in the field name (created_at_ms, not just created_at) rather than relying on convention. Our Unix timestamps explained guide covers how digit-count-based unit detection works when you do have to reverse-engineer an undocumented epoch field.

The short version

Epoch integers are compact but carry no unit; plain ISO 8601 is broad but can omit the offset; RFC 3339 is the narrow, unambiguous slice of ISO 8601 that most API tooling actually expects when it says “ISO date”. If you’re designing a new field and have no strong reason to optimize for payload size, RFC 3339 removes an entire category of cross-service timestamp bugs before it can start.

Frequently asked questions

Is RFC 3339 the same as ISO 8601?
RFC 3339 is a stricter profile of ISO 8601, not a separate format. It picks one unambiguous representation out of the many ISO 8601 permits — always a 4-digit year, always an explicit UTC offset (Z or ±hh:mm), no week-numbering dates, no reduced-precision dates like just '2026'. Any RFC 3339 timestamp is valid ISO 8601; the reverse isn't guaranteed.
Should a new API use epoch integers or a string timestamp?
For a new field, prefer an RFC 3339 string. It's self-describing (a log line or debugger shows the actual date, not a 10-digit number you have to decode), it round-trips across languages without an implicit unit agreement, and it carries its own UTC offset. Epoch integers earn their place when payload size matters (telemetry, high-frequency metrics) or when a field is purely an internal duration or ordering key.
Why do timestamp bugs so often come down to seconds vs. milliseconds?
Because an epoch integer carries no unit marker — 1700000000 is silently valid as both a 2023 date (seconds) and a 1970 date (milliseconds), so a wrong assumption doesn't error, it just produces a wrong-but-plausible-looking date. A string format sidesteps the whole class of bug because the precision is written out: 2023-11-14T22:13:20.500Z is self-evidently sub-second.
Does RFC 3339 require UTC?
No — it requires an explicit offset, which can be Z (UTC) or any ±hh:mm value. What it forbids is omitting the offset entirely, which ISO 8601 technically allows and which is the single biggest source of 'is this local or UTC?' ambiguity in loosely-typed date strings.

Sources & references

Authoritative references cited by this piece. Verified by Buğra Sözeri on the dates shown and re-checked at every deploy.

Related

More guides on this topic

Published September 25, 2026