Timezones and Unix Timestamps: Practical Rules for Developers
Time bugs usually start when code treats local clock time as a stable fact. Timezones, daylight saving rules, leap seconds, and historical changes all sit on top of one useful base representation: the Unix timestamp.
1. The Unix Timestamp: Time as a Single Integer
The Unix timestamp is the number of seconds elapsed since 00:00:00 UTC on January 1, 1970, the Unix epoch. It does not count leap seconds.
This 32-bit or 64-bit integer has useful properties:
- Monotonic and sortable: A larger timestamp always means a later moment. Sorting events chronologically requires comparing integers, not parsing date strings.
- Arithmetic-friendly: The difference between two timestamps is just subtraction. Adding 86,400 means "tomorrow at the same time" (most of the time — see DST below).
- Timezone-agnostic: A Unix timestamp encodes a single, unambiguous instant in time. What the local clock on the wall showed at that instant is a display concern, not a data concern.
This last point is the most misunderstood. 1700000000 represents the same physical moment whether you're in Tokyo, London, or New York. The local time displayed is a function applied to the timestamp, not part of it.
2. Practical Code: Timestamps Across Languages
The timestamp concept is universal, but the API varies:
JavaScript (milliseconds, not seconds):
const now = Date.now(); // ms since epoch
const seconds = Math.floor(now / 1000); // Unix timestamp
const date = new Date(seconds * 1000); // back to Date
const iso = new Date().toISOString(); // "2026-05-18T..."
Python:
import time
from datetime import datetime, timezone
now_ts = int(time.time()) # Unix timestamp
dt_utc = datetime.now(timezone.utc) # aware datetime
dt_ts = datetime.fromtimestamp(now_ts, tz=timezone.utc)
SQL (PostgreSQL):
SELECT EXTRACT(EPOCH FROM NOW()); -- current timestamp
SELECT TO_TIMESTAMP(1700000000); -- timestamp → readable
SELECT NOW() AT TIME ZONE 'Asia/Shanghai'; -- convert timezone
Use the same pattern in every language: store and transmit in UTC, then convert to local time at the display layer.
3. The Timezone Layer: Political, Not Astronomical
Timezones are where everything gets complicated. A few key concepts:
- UTC (Coordinated Universal Time): The global reference clock. All Unix timestamps are defined relative to UTC. It is technically neither a timezone nor GMT — though in practice, UTC and GMT are treated as equivalent for civil time.
- Offsets (UTC+8, UTC-5, etc.): A timezone's fixed offset from UTC. China uses a single offset (UTC+8) despite spanning five geographical timezones. Nepal uses UTC+5:45 — a 45-minute offset.
- DST (Daylight Saving Time): Roughly 70 countries shift their clocks forward by one hour during summer months. The switch dates vary by country and have changed multiple times throughout history. DST rules are a database, not an algorithm.
The IANA Time Zone Database (tzdata) is the definitive reference, maintained by volunteers who track every legislative time change worldwide. It's updated multiple times per year — because governments change their DST rules with little notice. In 2022, Iran abolished DST with six months' notice. In 2016, Turkey decided to stay on UTC+3 permanently. Every time a country changes its rules, every operating system and language runtime must update its tzdata copy.
4. Database Timezone Handling: TIMESTAMP vs TIMESTAMPTZ
Database timezone behavior varies dramatically across systems, and misunderstanding it causes silent data corruption. In PostgreSQL:
TIMESTAMP (without time zone): Stores whatever value you insert, with no timezone awareness. 2026-05-14 10:30:00 means exactly that wall-clock time — you don't know if it's Shanghai morning or New York morning. This is almost never what you want for data that crosses timezones.
TIMESTAMPTZ (with time zone): Stores the value internally as UTC after converting from the session timezone. When queried, it converts back to the session timezone for display. The stored UTC value is unambiguous; the display format is just rendering.
MySQL's TIMESTAMP type behaves like PostgreSQL's TIMESTAMPTZ (converts to UTC for storage, back to session timezone for display), while MySQL's DATETIME is timezone-naive. SQLite has no native timezone-aware type — timestamps are stored as text, integers, or floats, and timezone handling is entirely the application's responsibility.
The safe rule: use TIMESTAMPTZ in PostgreSQL, TIMESTAMP in MySQL (not DATETIME) for timezone-aware data, and in SQLite, store Unix timestamps as integers.
5. The Leap Second Problem
The Earth's rotation is gradually slowing. To keep UTC aligned with astronomical noon (the sun directly overhead), leap seconds are occasionally inserted — adding an extra second at 23:59:60 UTC.
For the Unix timestamp, this creates a fundamental tension. Unix time is defined as not counting leap seconds, which means:
- When a positive leap second occurs, the Unix timestamp either repeats (stays the same for two consecutive seconds) or smears (slows down slightly over a longer window).
- Different systems handle this differently. Google's NTP servers "smear" the leap second across a 24-hour window. Many financial systems pause trading during leap seconds to avoid timestamp ambiguity in transactions. AWS and Azure use leap smearing. Linux systems typically step the clock.
- Code that measures elapsed time by subtracting two Unix timestamps can be off by one second across a leap second event. For most applications this is harmless; for sub-second financial or scientific applications, it matters.
No single leap-second strategy works for every system, which is why standards bodies are moving toward abolishing leap seconds by 2035.
6. Common Pitfalls Developers Hit
Nearly every developer learns these the hard way:
- "I'll just store local time": If you store
2026-05-14 10:30 without a timezone, you have no idea when that moment actually occurred. Is this Asia/Shanghai morning or America/New_York morning? They're 12 hours apart.
- "I'll use the server's timezone": Servers should nearly always run in UTC and convert to local time only at the display layer. Otherwise, scaling to multiple regions or changing hosting providers introduces timezone bugs.
- "This ran fine yesterday": Code that works for 364 days a year can fail on DST transition days. A cron job scheduled for 2:30 AM in a region that springs forward from 2:00 to 3:00 AM will simply not run — 2:30 AM didn't exist that day. Conversely, a job scheduled for 1:30 AM during the fall-back transition runs twice.
- "I'll use the client's offset": An offset (e.g.,
UTC-5) is not a timezone. The offset for America/New_York switches between UTC-5 (winter) and UTC-4 (summer). If you store only the current offset, you cannot correctly compute "same time next week" — the offset may have changed by then.
- The Year 2038 Problem: 32-bit signed Unix timestamps overflow on January 19, 2038. Systems using 32-bit
time_t will wrap back to December 13, 1901. Most modern systems have migrated to 64-bit timestamps, but embedded devices, legacy databases, and older file formats remain at risk. If you maintain C code that stores time in a time_t or a 32-bit integer column, verify it is 64-bit.
7. Testing Time-Dependent Code
Time-dependent code must be testable without waiting for real time to pass. The standard technique is dependency injection of a clock:
- In JavaScript: inject
Date.now or use a library like sinon to stub it (sinon.useFakeTimers).
- In Python: use
freezegun to freeze time at a specific datetime during tests.
- In Go: pass a
clock interface rather than calling time.Now() directly.
- In all languages: write test cases that explicitly verify behavior across DST transitions (spring forward, fall back), across midnight, and across the epoch boundary.
A test suite that includes 2026-03-09T02:30:00 America/New_York (a nonexistent time — spring forward) and 2026-11-02T01:30:00 America/New_York (an ambiguous time — fall back, occurs twice) catches the most common timezone bugs before they reach production.
8. Best Practices for Handling Time
A few principles that prevent most time-related bugs:
- Store everything in UTC (as a timestamp or UTC datetime). Local time is a presentation concern. Convert at the last possible moment, in the presentation layer, with the user's timezone.
- Use the IANA timezone identifier (e.g.,
America/New_York), not the offset (e.g., UTC-5). The offset for America/New_York switches between UTC-5 and UTC-4 depending on DST. Hardcoding UTC-5 is wrong half the year.
- Use a well-maintained datetime library. Never write your own date math. For JavaScript:
date-fns or luxon (not Moment.js — it is in maintenance mode). For Python: zoneinfo (stdlib, 3.9+) or arrow.
- Never assume a day is 86,400 seconds long. DST transition days are 82,800 or 90,000 seconds. Leap seconds add further variance.
- Keep tzdata updated. Container images, CI runners, and deployment environments all carry a copy of the timezone database. Update it regularly, or use the language runtime's bundled copy where available.
Conclusion
Unix timestamps give you one unambiguous instant. Timezones, DST, leap seconds, and calendar systems add the complexity. Store time in UTC, use IANA timezone identifiers, test across DST boundaries, keep tzdata updated, and avoid writing your own date library.