Guide
Date, time and timezones explained: UTC, DST and ISO 8601
Every production system eventually mishandles a clock. A meeting reminder fires an hour early after daylight saving starts. A billing job double-charges because "midnight local" does not exist on spring-forward night. A blockchain explorer shows a transaction timestamp that confuses users in Tokyo and Toronto. The root cause is almost always the same: developers mixed up an instant (a single point on the universal timeline) with civil time (what a wall clock reads in a particular region). This guide explains the mental model that prevents those bugs — how to store UTC, parse ISO 8601, survive DST transitions, and test datetime logic across JavaScript, Python, and databases.
Instants, civil time, and why they are not interchangeable
An instant is unambiguous: "2026-06-07T18:00:00Z" names one moment everywhere on Earth. Civil time adds a timezone offset or named zone: "June 7, 2026 at 2:00 PM Eastern" only becomes an instant once you know whether Eastern is on standard time (UTC-5) or daylight time (UTC-4). The same wall-clock reading can map to two different instants during a fall-back overlap, or to no instant at all during a spring-forward gap.
UTC (Coordinated Universal Time) is the global reference for instants. It does not observe daylight saving. Servers, databases, and blockchains should store and compare instants in UTC (or as Unix epoch seconds/milliseconds, which are UTC-based by definition). Convert to local civil time only at the presentation layer — UI labels, calendar widgets, email copy — using the user's timezone preference.
Three rules that prevent most incidents:
- Store UTC, display local. Never persist "2:30 PM America/New_York" without also storing the offset or zone ID.
- Never do arithmetic on civil dates. "Add one day" to March 8 at 2:00 AM local can land on March 9 or skip an hour depending on DST. Add duration to instants instead.
- Use named zones, not fixed offsets, for future events.
UTC-5is wrong half the year in New York. Use IANA identifiers likeAmerica/New_York.
ISO 8601 and Unix timestamps
ISO 8601 is the interchange format APIs should speak. Key patterns:
2026-06-07T18:00:00Z— instant in UTC (theZmeans Zulu/UTC).2026-06-07T14:00:00-04:00— instant with explicit offset (same moment as the UTC example above during Eastern daylight time).2026-06-07— a calendar date without time; not an instant until you attach a timezone rule (dangerous for global products).
Always include the offset or Z in API responses. Parsing a string without
offset using the server's local timezone is a classic source of staging-vs-production
divergence.
Unix timestamps count seconds (or milliseconds) since 1970-01-01T00:00:00Z.
They are compact, sortable, and timezone-agnostic — ideal for logs, caches, and on-chain
block times. Downsides: not human-readable, year-2038 limits on 32-bit signed seconds,
and millisecond vs second confusion between JavaScript (Date.now()) and
many Unix tools. Document which unit your API uses and never mix them silently.
Daylight saving: gaps, overlaps, and political surprises
Most of North America and Europe shift clocks twice a year. Two edge cases break naive code:
Spring forward (gap)
On the US "spring forward" Sunday, 2:00 AM becomes 3:00 AM. Local times between 2:00 and 2:59 do not exist. Scheduling "2:30 AM" that day should error or shift — not silently pick the wrong instant.
Fall back (overlap)
On the fall-back Sunday, 1:00 AM happens twice — once in daylight time, once in standard time. "1:30 AM" is ambiguous. Your library must know which occurrence the user meant, usually by offset or by "first" vs "second" disambiguation.
DST rules also change by government decree. Egypt, Turkey, and US states have altered schedules with short notice. That is why production systems rely on the IANA Time Zone Database (tzdata), updated several times per year, rather than hard-coded offset math.
JavaScript: Date pitfalls and the Temporal migration
The built-in Date object is a notorious footgun. It stores one instant
internally but many methods (getHours(), getMonth()) interpret
that instant in the runtime's local timezone — fine on a laptop in
Chicago, wrong in CI running UTC.
Common mistakes:
new Date('2026-06-07')— parsed as UTC midnight in ES5 but as local midnight in some engines for date-only strings. Prefer full ISO with offset.date.setHours(0, 0, 0, 0)— "start of day" in local time; not portable across users.- Comparing
Dateobjects with==instead ofgetTime(). - Formatting with manual string concatenation instead of
Intl.DateTimeFormat.
For display, use Intl:
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'medium',
timeStyle: 'short'
}).format(new Date('2026-06-07T18:00:00Z'));
The Temporal proposal (available in modern browsers and Node via
polyfill) replaces Date with explicit types: Temporal.Instant
for timeline points, Temporal.ZonedDateTime for zone-aware civil time,
Temporal.PlainDate for calendar dates without time. The API makes illegal
DST times throw instead of guessing — the behavior you want in financial and scheduling
apps.
Because datetime work still runs on the main thread, heavy parsing in hot paths can hurt Interaction to Next Paint. Cache formatted strings and defer bulk conversions to Web Workers when profiling shows a problem.
Python, Node.js backends, and databases
In Python, prefer datetime with explicit timezone info
(datetime.now(timezone.utc)) or the third-party zoneinfo module
(stdlib since 3.9) over naive datetimes. Never compare naive and aware objects.
Node.js services
should serialize JSON with ISO strings including Z, set
TZ=UTC in containers, and log epoch milliseconds with a documented field
name. Cron jobs scheduled in "local server time" break when the host timezone differs
from the business timezone.
Database patterns:
- PostgreSQL:
timestamptzstores UTC internally; casts display to the session timezone. Usetimestamptzfor instants,datefor calendar dates without time. - MySQL:
DATETIMEis naive;TIMESTAMPconverts to UTC on write. Know which you chose and stay consistent. - SQLite: often stores ISO text or integers; enforce UTC at the application layer.
For recurring events ("every Tuesday at 9 AM in London"), store the rule (RRULE or custom) plus the zone ID — not a series of precomputed UTC instants that break when Parliament moves DST.
API design and cross-timezone products
REST APIs should accept and return instants in UTC with ISO 8601. If users submit local civil times (booking forms), require a timezone field and validate DST edges server-side. Return both the instant and the user's intended local representation when disputes matter.
Pagination cursors based on local midnight ("transactions since start of today") are
undefined for global users unless "today" is scoped per user zone. Prefer
since=2026-06-07T00:00:00-04:00 or pass the user's IANA zone as a query
parameter.
Blockchain UIs face a special case: block timestamps are instants (Unix seconds), but users expect local labels. Always label the timezone in the UI ("14:32 UTC" or "10:32 EDT") so a Solana transaction viewed in Berlin and Boston does not look like a bug.
Testing: fixtures that catch DST bugs
Unit tests written only in July miss half the year's edge cases. Maintain a small fixture table and run it in CI:
- US spring-forward gap:
America/New_York, 2026-03-08 02:30 (invalid) - US fall-back overlap:
America/New_York, 2026-11-01 01:30 (ambiguous) - UTC boundary:
2026-12-31T23:59:59Zrolling into new year - Leap second / leap day:
2024-02-29for calendar-date logic - Southern hemisphere DST:
Australia/Sydney(opposite season to US)
Property-based tests that add random durations to instants and round-trip through format
parsers often find offset bugs faster than hand-picked examples. Freeze the clock in tests
(jest.useFakeTimers, Python freezegun) but still vary the
timezone environment variable.
Production checklist
- Store and compare instants in UTC (or Unix epoch); convert to local only for display.
- Serialize ISO 8601 with explicit
Zor offset in every API field. - Use IANA timezone names (
Europe/London), not fixed UTC offsets, for future local times. - Validate DST gaps and overlaps; never assume every local time exists.
- Run containers and CI in UTC; document the business timezone separately.
- Pick
timestamptz(or equivalent) in new database schemas for events. - Add DST fixture dates to your test suite; update tzdata when OS packages release.
- Label timezones in user-facing timestamps so global audiences trust the UI.
Related reading
- JavaScript event loop explained — scheduling, timers, and main-thread performance
- Python fundamentals explained — datetime and zoneinfo patterns in backend services
- Node.js fundamentals explained — server runtime, JSON APIs, and production logging
- REST API design explained — consistent datetime fields in HTTP contracts