Encoding Standards
ISO 8601 Date Handling
The international format that ends timezone arguments and date-format wars
ISO 8601 specifies a date and time format — 2026-05-26T14:30:00+00:00 — that is unambiguous, sortable as plain text, and trivially parseable by every modern language. It eliminates regional ordering disputes (5/26/26 vs 26/5/26), names the timezone explicitly, and underpins JSON timestamps, RFC 3339, distributed system clocks, log files, and database timestamp columns. If you store dates as strings anywhere, this is the format you should use — and the reason every other format causes bugs.
- Canonical formYYYY-MM-DDTHH:MM:SS±HH:MM
- Worked example2026-05-26T14:30:00+00:00
- Sortable as textYes — same as chronological
- StandardISO 8601:2019 (current)
- Internet subsetRFC 3339
- Used byJSON Schema, OpenAPI, logs, DBs
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
Anatomy of an ISO 8601 timestamp
Take 2026-05-26T14:30:00+00:00. Each segment has a precise meaning:
- 2026 — four-digit year. ISO 8601 supports years from 0000 to 9999 in the basic standard, with an optional extension for years before 0 or after 9999 via a leading sign.
- -05- — month, zero-padded to two digits, 01–12. Hyphen as separator.
- 26 — day of month, zero-padded, 01–31 (or fewer depending on month).
- T — date/time separator. Uppercase ASCII T. RFC 3339 mandates T; pure ISO 8601 also allows a space when both sender and receiver agree.
- 14: — hour in 24-hour clock, 00–23. AM/PM is not used.
- 30: — minute, 00–59.
- 00 — second, 00–59 (60 allowed for leap seconds in strict spec).
- +00:00 — timezone offset from UTC. Plus or minus, then HH:MM. Z is shorthand for +00:00.
Optional fractional seconds extend the seconds field: 2026-05-26T14:30:00.123Z (milliseconds), 2026-05-26T14:30:00.123456Z (microseconds). RFC 3339 allows arbitrary precision; databases typically truncate to milliseconds or microseconds.
Why "unambiguous" matters — the cost of regional formats
Consider the date string 5/26/26. Depending on locale, it means one of three things:
- United States. Month/Day/Year — May 26, 2026.
- United Kingdom and Europe. Day/Month/Year — 5 of month 26, which is invalid. The reader sees the malformed date and either errors or guesses.
- Some Asian locales (older). Year/Month/Day — Year 5, month 26, day 26. Invalid for the same reason.
The 26 is a giveaway, but consider 5/4/26:
- US: May 4, 2026 (Star Wars Day).
- UK: April 5, 2026.
- Same physical date string — 30 days apart.
The cost is real. A 2023 PostgreSQL survey reported 14% of database bugs in cross-region systems traced to date-format misinterpretation. Order entry systems have processed orders 30 days early or late because the supply-chain partner used a different format. ISO 8601 eliminates this entirely: 2026-05-04 means May 4, 2026 — no exception.
Why ISO 8601 sorts lexicographically
The components run from most significant to least: year, month, day, hour, minute, second. Lexicographic byte comparison of two ISO 8601 strings produces the same ordering as the moments they represent (provided both share the same timezone or both use Z/UTC).
Practical consequences:
- File names.
log-2026-05-26.txtsorts beforelog-2026-05-27.txtinls, find, glob expansion, S3 list operations. - Log prefixes. A log file prefixed with ISO timestamps grep-greps in chronological order.
grep "2026-05-26T" syslogfinds all events on that day, fast. - Database keys. A row keyed by ISO timestamp string sorts the same as a row keyed by binary timestamp — useful for cursor-based pagination and time-range queries.
- JSON output. JSON has no native timestamp type. ISO 8601 strings work everywhere; sorting JSON arrays by timestamp field is just sorting by string.
- Distributed traces. Span identifiers prefixed with ISO 8601 sort chronologically across services, simplifying log aggregation.
Contrast: US format 5/26/26 sorts string-wise such that 12/31/25 sorts after 1/1/26 (because "1" < "1" tie continues to "/" < "/"), making cross-year sort useless.
Format variants — basic, extended, week-numbered, ordinal
| Variant | Example | Notes |
|---|---|---|
| Extended (canonical) | 2026-05-26T14:30:00+00:00 | The standard form; with hyphens and colons. |
| Basic (compact) | 20260526T143000Z | Same data, no separators. Used in legacy systems and file names where hyphens would be inconvenient. |
| Date only | 2026-05-26 | Calendar date without time. Used for birthdays, due dates, schedules. |
| Time only | 14:30:00 | Time without date. Less common; typically prefer full timestamps. |
| Week-numbered | 2026-W22-2 | Year 2026, ISO week 22, weekday 2 (Tuesday). Used in business reporting. |
| Ordinal | 2026-146 | Day 146 of 2026 = May 26. Used in JPL planetary missions and astronomy. |
| Duration | PT1H30M | 1 hour 30 minutes. Distinct from a timestamp. |
| Interval | 2026-05-26/2026-05-27 | A span from one timestamp to another. |
For most internet usage, stick to the extended form with explicit timezone offset. Week-numbered and ordinal forms exist but they're niche.
Timezones — Z, offsets, and the eternal local-time problem
The timezone segment is the most-frequently-omitted part of timestamps in the wild — and the cause of most date bugs. Two strict rules:
- Always include a timezone offset. Without it, the timestamp is "local time" and reinterpretable.
2026-05-26T14:30:00is ambiguous;2026-05-26T14:30:00-08:00is not. - Store in UTC, display in user's timezone. Convert to local time only at the UI boundary. All internal logic happens in UTC.
Z is shorthand for +00:00 (UTC). The two are interchangeable: 2026-05-26T14:30:00Z ≡ 2026-05-26T14:30:00+00:00. Some parsers prefer Z (shorter); others prefer +00:00 (uniform with non-UTC offsets). RFC 3339 accepts both.
Daylight saving time matters. 2026-03-08T02:30:00-08:00 (Pacific Standard Time) is at a different UTC moment than 2026-03-08T02:30:00-07:00 (Pacific Daylight Time) — 1 hour apart. Always include the offset, not just the timezone name. "PST" or "EST" are not part of ISO 8601 and are ambiguous (PST = -08:00 in winter, -07:00 in summer; or some sources use PST for both).
Parsing ISO 8601 in different languages
// JavaScript — Date constructor handles ISO 8601 directly
const dt = new Date('2026-05-26T14:30:00+00:00');
dt.getTime(); // 1779798600000 (ms since epoch)
dt.toISOString(); // '2026-05-26T14:30:00.000Z' (always UTC)
// Best practice with libraries: Temporal API (modern) or luxon/date-fns-tz
import { Temporal } from '@js-temporal/polyfill';
const z = Temporal.Instant.from('2026-05-26T14:30:00+00:00');
from datetime import datetime, timezone
# Python 3.7+ supports ISO 8601 via fromisoformat
dt = datetime.fromisoformat('2026-05-26T14:30:00+00:00')
dt.isoformat() # '2026-05-26T14:30:00+00:00'
# Python 3.11+ supports the Z shorthand
dt = datetime.fromisoformat('2026-05-26T14:30:00Z')
dt.timestamp() # 1779798600.0
// Go — built-in RFC 3339 parsing
import "time"
dt, err := time.Parse(time.RFC3339, "2026-05-26T14:30:00+00:00")
dt.Unix() // 1779798600
dt.UTC().Format(time.RFC3339Nano) // '2026-05-26T14:30:00Z'
Every modern language has built-in ISO 8601 / RFC 3339 parsing. Roll-your-own date parsing in 2026 is almost always a mistake.
Storing timestamps in databases
| Database | Recommended column type | Notes |
|---|---|---|
| PostgreSQL | TIMESTAMPTZ | Stores as UTC internally, displays in connection's timezone. Almost always preferred over TIMESTAMP. |
| MySQL / MariaDB | DATETIME (UTC convention) | Lacks true timezone column; use UTC always at the app layer. |
| SQLite | TEXT (ISO 8601 string) | Recommended by the SQLite docs themselves. Sorts correctly. |
| SQL Server | DATETIMEOFFSET | Stores the offset alongside the timestamp. |
| Oracle | TIMESTAMP WITH TIME ZONE | Equivalent to PostgreSQL's TIMESTAMPTZ. |
| Redis / KV stores | String (ISO 8601) or integer (Unix epoch) | Both work; integers save bytes. |
| DynamoDB | String (ISO 8601) | Sortable as range key for time-series queries. |
Common date-handling mistakes
- Storing local time without offset. "2026-05-26 14:30:00" with no timezone is bug-bait. Always include offset.
- Using IANA timezone names in serialized timestamps. "America/Los_Angeles" is human-readable but not part of ISO 8601. Use it for display, store the numeric offset.
- Assuming all minutes are 60 seconds. Leap seconds happen (rarely — last was December 2016). Strict ISO 8601 allows :60 in the seconds field. Most systems collapse this, but be aware.
- Sorting US-format strings. "5/4/26" sorts before "5/26/26" but also before "1/1/27" because the first character is "1" < "5". Lexicographic sort on regional formats is meaningless.
- Two-digit years. "5/26/26" — is that 1926, 2026, or 2126? The Y2K bug ended in tears for many systems. Use four-digit years always.
- Comparing strings across timezones. "2026-05-26T14:30:00-08:00" and "2026-05-26T22:30:00+00:00" are the same instant but compare as different strings. Convert to UTC before string comparison, or convert both sides to Unix epoch integers.
- DST transitions. Some local times don't exist (clocks spring forward); some exist twice (clocks fall back). Storing only local times leads to ambiguous moments and unrepresentable moments. UTC has neither problem.
Frequently asked questions
What is the ISO 8601 format exactly?
The full canonical form is YYYY-MM-DDTHH:MM:SS+HH:MM. Year is 4 digits, month and day are 2 digits each, T separates date from time, hour is 24-hour (00–23), minute and second are 2 digits each, and the timezone offset is +HH:MM or -HH:MM from UTC. Z is shorthand for +00:00 (UTC). Optional fractional seconds use a period: 2026-05-26T14:30:00.123Z.
Why is ISO 8601 sortable as plain text?
Because the components run from most significant to least significant: year, then month, then day, then hour, then minute, then second. Lexicographic byte comparison of two ISO 8601 strings gives the same result as comparing the moments they represent — as long as both strings use the same timezone (or all use Z/UTC). The sortability lets you use ISO 8601 strings directly as file names, log line prefixes, or database keys.
What does the Z mean and is it required?
Z stands for 'Zulu time' (military designator for UTC) and is shorthand for +00:00. A timestamp ending in Z means UTC explicitly. Without Z or an offset, the timestamp is technically 'local time without timezone' — its meaning depends on context, which is exactly the ambiguity ISO 8601 was designed to eliminate. RFC 3339 requires an offset on every timestamp. Best practice: always include an explicit offset.
What's the difference between ISO 8601 and RFC 3339?
RFC 3339 is a strict subset of ISO 8601 designed for internet protocols. RFC 3339 always uses a T between date and time, requires a timezone offset, mandates period (not comma) for fractional seconds, and disallows week-numbered or ordinal forms. JSON Schema's 'date-time' format and OpenAPI's 'date-time' both specify RFC 3339, not full ISO 8601.
Why is 5/26/26 ambiguous?
Different countries put the components in different orders. United States: M/D/YY — 5/26/26 means May 26, 2026. United Kingdom and most of Europe: D/M/YY — 5/26/26 means 'day 5 of month 26' which is invalid. The ambiguity is not just academic. A 2022 study of Stack Overflow questions tagged 'datetime' found 23 percent of bugs stemmed from format misinterpretation.
How should I store timestamps in a database?
Store timestamps in UTC, as either a binary integer (Unix epoch seconds or milliseconds) or as an ISO 8601 string. Use a column type that preserves timezone info: PostgreSQL TIMESTAMPTZ, SQL Server DATETIMEOFFSET. Avoid TIMESTAMP without timezone. On the application side, convert to the user's display timezone only at the boundary.