Unix Timestamps & Timezones: Epoch Math, DST Pitfalls, and Leap Seconds
Time looks simple โ a number counting seconds โ until daylight saving time deletes an hour, a leap second makes a minute 61 seconds long, and your database stores "2026-03-08 02:30" in a timezone where that moment never existed. Most time bugs come from one root cause: confusing instants (points on the timeline) with wall-clock readings (what a clock on a wall shows). Get that distinction right and 90% of time bugs disappear.
The Unix Epoch: Seconds Since 1970
A Unix timestamp is the number of seconds elapsed since 1970-01-01 00:00:00 UTC, not counting leap seconds. 1516239022 means 1,516,239,022 seconds after the epoch โ 2018-01-18 01:30:22 UTC.
Three properties make timestamps the right way to store and transmit instants:
- Unambiguous. A timestamp names one instant, worldwide. No timezone, no DST, no format disputes.
- Sortable and comparable. Earlier instant = smaller number. Arithmetic works:
exp - iatis the token's lifetime in seconds. - Compact. A 64-bit integer covers ยฑ292 billion years. (The 32-bit version famously overflows on 2038-01-19 03:14:07 UTC โ the Y2K38 problem. If any of your systems still use 32-bit
time_t, that date is your deadline.)
Converting in code is trivial:
from datetime import datetime, timezone
# Instant -> timestamp
dt = datetime(2026, 10, 18, 12, 0, 0, tzinfo=timezone.utc)
ts = int(dt.timestamp()) # 1792315200
# Timestamp -> instant (always via UTC)
back = datetime.fromtimestamp(ts, tz=timezone.utc)
The critical detail: always convert through UTC. datetime.fromtimestamp(ts) without a timezone uses the server's local timezone โ the same code returns different wall-clock times on servers in different regions. That single missing argument has caused more production incidents than any other datetime bug.
Wall Clock vs Instant: The Two Kinds of Time
- An instant is a point on the universal timeline. Represent it as a Unix timestamp or a UTC datetime.
- A wall-clock reading is what a clock shows in some timezone: "2026-10-18 09:00 in America/New_York." The same wall-clock reading can mean different instants (or none at all) depending on DST.
Storage rule: store instants, display wall-clock. Databases should hold UTC timestamps (or timestamptz); convert to the user's timezone only at the presentation layer. The moment you store "2026-11-01 01:30" without a timezone, you've lost information โ on the US fall-back day, that wall-clock reading occurs twice.
DST: The Hour That Vanishes (and the Hour That Repeats)
Daylight saving time shifts clocks forward in spring and back in fall. In America/New_York:
- Spring forward (2026-03-08): at 02:00 EST the clock jumps to 03:00 EDT. The wall-clock times 02:00โ02:59 do not exist. A cron job scheduled for 02:30 simply never runs that day; a user booking "02:30" has booked a nonexistent moment.
- Fall back (2026-11-01): at 02:00 EDT the clock returns to 01:00 EST. The wall-clock times 01:00โ01:59 occur twice โ once in EDT (UTC-4), once in EST (UTC-5). "01:30" is ambiguous without specifying which occurrence.
This is why scheduling must be done in a timezone-aware way:
from zoneinfo import ZoneInfo
from datetime import datetime, timedelta
ny = ZoneInfo("America/New_York")
# "Same time tomorrow" done right: add a calendar day, not 24 hours
meeting = datetime(2026, 3, 7, 9, 0, tzinfo=ny) # 9 AM EST
next_day = meeting + timedelta(days=1) # 9 AM EDT -- wall clock preserved
# timedelta(hours=24) would give 10 AM -- wrong for a daily standup
Adding timedelta(days=1) to an aware datetime preserves the wall-clock time across the DST boundary; adding 24 hours preserves the elapsed duration. For human schedules you almost always want the former; for timeouts and SLAs you want the latter. Know which one you're doing.
Also note: not every zone observes DST, and rules change โ governments do alter DST dates with little notice. Never hardcode offsets like "EST = UTC-5"; always use the IANA timezone database (America/New_York, not EST), which encodes the full history and is updated several times a year.
Leap Seconds: The 61-Second Minute
Earth's rotation is irregular, so UTC occasionally inserts a leap second: a minute with 61 seconds, written 23:59:60. The last one was 2016-12-31; more will come unpredictably.
Unix timestamps pretend leap seconds don't exist โ the timestamp just repeats or the extra second is smeared. This creates real engineering responses:
- Google's leap smear: over a 24-hour window around the leap second, Google's NTP servers stretch each second by a tiny fraction, so no second is ever numbered 60. Clocks stay monotonic โ no
23:59:60ever appears. - Linux kernels by default step the clock, which can make the same timestamp occur twice or jump backward โ a hazard for anything assuming monotonic time.
Practical consequences for you: never assume t2 - t1 over a leap second equals the nominal duration at sub-second precision, and never use wall-clock time to measure elapsed durations โ use a monotonic clock (time.monotonic() in Python, performance.now() in JS, CLOCK_MONOTONIC in C) for timeouts, benchmarks, and rate limiters. Wall clocks can jump; monotonic clocks can't.
Common Mistakes
1. Storing local time without a timezone. 2026-11-01 01:30 is ambiguous in fall-back zones and meaningless across regions. Store UTC; convert on display.
2. Using the server's local timezone implicitly. datetime.now() without tzinfo produces a naive datetime that behaves differently per server. Always use datetime.now(timezone.utc).
3. Stringly-typed time in APIs. "10/18/2026" โ is that October 18 or 10 August? Use ISO 8601 (2026-10-18T12:00:00Z) or Unix timestamps in APIs. The Z suffix means UTC; an offset like +08:00 is explicit. A datetime string with neither is a bug waiting to happen.
4. Assuming a day is 86,400 seconds. DST days are 82,800 or 90,000 seconds; leap-smear days are slightly off too. Date arithmetic must go through a calendar-aware library, not multiplication.
5. Comparing timestamps as floats. Millisecond timestamps as floats lose integer precision beyond 2^53 (~year 2255 for ms โ fine) but microsecond floats lose precision now. Keep sub-second timestamps as integers.
6. Scheduling in UTC for humans. "The report runs at 09:00 UTC daily" drifts against local time when DST changes. If humans expect 9 AM local, schedule in the local IANA zone and let the library handle the transitions โ including deciding what "02:30 on spring-forward day" means (skip, or run at 03:30; pick explicitly).
Debugging Walkthrough: "Events Are an Hour Off Since Sunday"
- Check whether DST just changed in the affected timezone. An off-by-exactly-one-hour shift appearing overnight is DST until proven otherwise.
- Find where the conversion happens. Log the UTC timestamp and the displayed local time. If the timestamp is correct but the display is wrong, the bug is in the formatting layer's timezone.
- Look for hardcoded offsets. Search the codebase for
-5,-4,+8,EST,PSTused as fixed offsets. Any of these breaks twice a year. - Check the tz database version. Containers and serverless runtimes sometimes ship stale tzdata. If a government changed DST rules recently, old tzdata applies the old rules.
- Distinguish display from storage. If the stored timestamps shifted, you have a data-corruption bug (something wrote local time as UTC). If only the display shifted, it's a presentation bug. These have very different fixes.
Try It
Epoch math is best learned interactively. DailyToolbox's free timestamp converter runs entirely in your browser โ convert between Unix timestamps and human dates, see the same instant in any timezone, and check what "now" looks like as a number.
FAQ
Should I use seconds or milliseconds for timestamps?
Milliseconds are the web default (JavaScript's Date.now()), seconds are the Unix tradition. Either is fine โ but be explicit and consistent. A 13-digit number is milliseconds; a 10-digit number is seconds. Mixing them up (treating ms as s) puts you in the year 50,000.
What's the difference between GMT and UTC?
For most programming purposes, nothing โ both refer to the zero-offset time standard. Technically, GMT is an astronomical time scale and UTC is an atomic-clock-based standard with leap seconds, but APIs and libraries treat them as equivalent. Prefer the label "UTC."
How should I handle birthdays and anniversaries?
These are calendar dates, not instants โ someone born on 2000-02-29 doesn't age a second at a timestamp. Store them as date-only values (2000-02-29) with no time or timezone. Converting a birthday to a timestamp introduces timezone bugs for zero benefit.
Why does my cron job skip a day (or run twice) in spring/fall?
Cron daemons evaluate schedules in a timezone โ often the system local zone. On spring-forward day, 02:30 doesn't exist (job skipped); on fall-back day, 01:30 occurs twice (job runs twice). Run cron in UTC and convert, or accept and document the behavior for local-time schedules.
Are leap seconds going away?
Possibly. In 2022 the CGPM (the body behind the SI units) voted to eliminate leap seconds by 2035, letting UTC drift from Earth's rotation instead. Until then, systems must keep handling them โ and the smearing techniques aren't going anywhere.