←Back to Guides
โฐDistributed Systemsโฑ๏ธ 15 min read•Updated October 2026

Time, Timezones & Epoch Engineering in Distributed Systems

The battle-tested engineering handbook for handling time: Master Unix epoch seconds vs milliseconds, eliminate Daylight Saving Time (DST) bugs, configure PostgreSQL timestamptz correctly, and debug NTP clock skew.

#Timezones#Epoch#Distributed Systems#PostgreSQL#JavaScript#Python#Architecture

1. The Four Golden Rules of Time in Software Engineering

Time is notoriously deceptive in distributed systems due to leap seconds, daylight saving adjustments, server clock drifts, and timezone offsets.
The Architecture Principles
/* RULE 1: Internal representation MUST always be UTC or Unix epoch milliseconds.
 * RULE 2: Store timestamps in databases as UTC; convert to user timezones exclusively at the UI edge.
 * RULE 3: Store recurring schedules as (Local Time + IANA Timezone Identifier), NOT as UTC!
 *         Why? When governments change DST laws, UTC shifts, but the user expects the alarm at 09:00 local!
 * RULE 4: Always use IANA zone names ("Europe/Amsterdam", "America/New_York"), NEVER 3-letter codes ("EST", "BST").
 *         Why? "CST" represents Central Standard Time (US), China Standard Time, AND Cuba Standard Time!
 */

Three-letter timezone abbreviations are ambiguous and cannot compute daylight saving transitions.

ConceptSpecificationExampleBest Use CaseCopy
Unix Epoch (Seconds)POSIX time (10 digits)1792761600JWT exp claims, Linux kernel and file timestamps
Unix Epoch (Milliseconds)JavaScript Date.now() (13 digits)1792761600000Node.js and browser timestamps
ISO 8601 / RFC 3339YYYY-MM-DDTHH:mm:ss.sssZ2026-10-23T13:20:00.000ZREST APIs and JSON payloads
IANA Timezone IdentifierArea/Location formatEurope/AmsterdamUser profiles and recurring schedules (DST-aware)

2. ISO 8601 vs RFC 3339: Parsing and String Formats

While ISO 8601 allows numerous optional representations (including basic format without dashes: 20261023), RFC 3339 specifies a strict profile designed for internet protocols. Always format API outputs with complete timezone indicators (either "Z" for Zulu/UTC or an explicit offset like "+02:00").
Defensive Timestamp Formatting in JavaScript
// CORRECT: Produces unambiguous ISO 8601 / RFC 3339 UTC string
const now = new Date();
const apiString = now.toISOString(); // "2026-10-23T13:16:13.123Z"

// CAUTION with JavaScript Date Parsing:
// In older engines, "2026-10-23" was parsed as UTC, while "2026/10/23" was parsed as local time!
// Always validate against full ISO strings with timezone suffixes:
function parseSafeDate(isoString: string): Date {
  if (!/Z|[+-]\d{2}:?\d{2}$/.test(isoString)) {
    throw new Error(`Unsafe timestamp without timezone offset: ${isoString}`);
  }
  const d = new Date(isoString);
  if (isNaN(d.getTime())) throw new Error(`Invalid date string: ${isoString}`);
  return d;
}

A date string without a trailing "Z" or "+/-HH:mm" offset defaults to unpredictable host-local interpretations.

3. Database Storage: PostgreSQL timestamptz vs MySQL DATETIME

Different database engines treat timestamps radically differently. In PostgreSQL, "TIMESTAMP WITHOUT TIME ZONE" is an anti-pattern that discards offset context.
PostgreSQL vs MySQL Timestamp Behavior
-- POSTGRESQL:
-- Always use TIMESTAMPTZ (internally stored as pure 8-byte UTC microsecond integer!)
CREATE TABLE audit_logs (
    id BIGSERIAL PRIMARY KEY,
    event_name VARCHAR(100) NOT NULL,
    occurred_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);
-- Note: clock_timestamp() gives the actual current time, unlike NOW() which freezes at transaction start!

-- MYSQL:
-- TIMESTAMP stores UTC (limited to Year 2038 problem!)
-- DATETIME stores raw literal numbers without timezone awareness (up to Year 9999).
-- In MySQL 8+, configure default time_zone = "+00:00" and use DATETIME.

TIMESTAMPTZ in Postgres does NOT store the timezone name; it converts input to UTC and outputs in the client session timezone.

4. Daylight Saving Time (DST) & The Missing Hour Trap

Twice a year, clocks skip forward from 01:59:59 to 03:00:00 (the missing 60 minutes) or fall back from 02:59:59 to 02:00:00 (the duplicated 60 minutes). Doing naive arithmetic by adding 86,400 seconds (24 hours) to a local timestamp produces wrong calendar dates across DST boundaries.
Handling DST in JavaScript / TypeScript
// ANTI-PATTERN: Naive 24-hour addition breaks across DST switch:
const oneDayMs = 24 * 60 * 60 * 1000;
const tomorrow = new Date(today.getTime() + oneDayMs); // Might be 23 or 25 hours local!

// PRODUCTION PATTERN: Use Intl or modern Temporal API / date-fns / luxon
// In standard JavaScript, adjust calendar day explicitly:
const nextDay = new Date(today);
nextDay.setDate(today.getDate() + 1); // The browser runtime adjusts for DST automatically!

// Display in target user timezone accurately:
const formatted = new Intl.DateTimeFormat("nl-NL", {
  timeZone: "Europe/Amsterdam",
  dateStyle: "full",
  timeStyle: "long"
}).format(today);

Intl.DateTimeFormat leverages the operating system and browser IANA tzdb for exact historical and future DST offsets.

5. Clock Skew, NTP & Monotonic Timers

Server clocks drift. Even with Network Time Protocol (NTP), machines can differ by tens of milliseconds. Never use system wall-clock time (Date.now() or time.time()) to measure elapsed execution durations, because NTP synchronization steps can cause wall-clock time to jump backwards!
Using Monotonic Clocks for Benchmarks & Timeouts
// ANTI-PATTERN: Wall-clock can jump backwards during NTP sync
const start = Date.now();
await doWork();
const duration = Date.now() - start; // Can occasionally be negative!

// PRODUCTION PATTERN: Monotonic clock (strictly increments, immune to clock drift)
const startMonotonic = performance.now(); // High-resolution milliseconds
await doWork();
const elapsedMs = performance.now() - startMonotonic;

// In Linux CLI: check NTP sync and server clock drift
timedatectl status

performance.now() (Node.js and browsers) and CLOCK_MONOTONIC in C/Go/Rust guarantee strictly positive elapsed time.

๐ŸŽ“Explore More Guides

โœจ