The Timezone Nightmare: Why You Should Never Do Date Math
"I'll just add 24 hours to get tomorrow." Famous last words. Here is why time is the hardest problem in distributed systems and how to survive it.
There are two types of developers: those who fear timezones, and those who have not worked with them yet. It starts innocently. A user in London creates a meeting for 9:00 AM. A user in New York sees it at... 9:00 AM? That is wrong. A cron job fires at midnight UTC but it is yesterday in California. A "date range" query misses the last day because the database stores UTC but the comparison uses local time.
Time handling is uniquely treacherous because the bugs are intermittent — they only appear on certain days of the year, in certain timezones, around midnight, or during Daylight Saving transitions. This guide covers the traps that catch real developers in production, and the concrete rules that prevent them.
1. The "Add 24 Hours" Trap
New developers often think: "To get tomorrow's date, I will take today's Unix timestamp and add 86,400 seconds (24 × 60 × 60)."
This works on most days. It fails spectacularly twice a year.
When Daylight Saving Time begins (clocks spring forward), that particular Sunday is only 23 hours long. If you add 86,400 seconds to a timestamp near midnight, you land at 1:00 AM the next day — effectively skipping a day's boundary in your logic. When DST ends (clocks fall back), the day is 25 hours long, causing the opposite problem.
// WRONG — breaks on DST transition days
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
// CORRECT — add one calendar day regardless of DST
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
The setDate() approach works because JavaScript's Date object handles DST
transitions when you manipulate calendar fields. The arithmetic approach does not — it operates
on raw milliseconds and has no concept of calendar days.
2. DST: The Full Picture
Daylight Saving Time is observed differently in nearly every country that uses it. The US, Europe, and Australia change on different dates. Some regions within a country observe DST and others do not (parts of Indiana, Arizona, and various Australian territories). Brazil abolished DST in 2019. Russia abolished it in 2014. Turkey moved to permanent summer time.
This means a "timezone offset" like UTC+3 is not stable over time. Europe/Istanbul
is UTC+3 year-round since 2016. Europe/Moscow is UTC+3 year-round since 2014. Europe/Helsinki
alternates between UTC+2 and UTC+3. They produce the same offset in winter but different
behaviour in summer.
The rule: Never store a timezone as a raw offset like +05:30.
Store a named timezone identifier like America/New_York or
Asia/Kolkata. The IANA timezone database (bundled in modern OS and browsers)
knows the full history of DST rules for each named zone.
3. Server Time vs. User Time
If your server is in Virginia (AWS us-east-1) and your user is in Tokyo, new Date()
on the server returns US Eastern time. new Date() in the user's browser returns
Japan Standard Time. If you format a date on the server and send it as a string, the user sees
the wrong time.
The golden rule: always store in UTC.
UTC (Coordinated Universal Time) is the invariant reference. It has no Daylight Saving Time. It never changes. Every timestamp in your database should be UTC. Every timestamp in your API responses should be UTC. Convert to local time at the final step — in the UI, using the user's timezone.
// Server: store as UTC ISO string
const createdAt = new Date().toISOString();
// "2026-04-04T14:32:00.000Z" ← always UTC, the Z means UTC
// Client: display in user's local timezone
const localTime = new Date(createdAt).toLocaleString('en-US', {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
dateStyle: 'medium',
timeStyle: 'short'
});
The Intl.DateTimeFormat().resolvedOptions().timeZone call reads the user's system
timezone — it returns an IANA identifier like "America/Chicago".
4. UTC vs. GMT: They Are Not the Same
UTC and GMT are commonly used interchangeably, and for most practical purposes the difference does not matter — they share the same current offset (both are at zero hours). But they are technically different:
- GMT (Greenwich Mean Time) is a timezone — specifically, the timezone of the Greenwich meridian in London. During British Summer Time, the UK moves to GMT+1.
- UTC (Coordinated Universal Time) is an atomic time standard. It does not have DST adjustments. It never changes. It is the ground truth.
Always use UTC in code. When documentation says "GMT," it usually means UTC, but if precision matters, clarify.
5. ISO 8601: The Only Correct Date String Format
Stop storing dates as strings like "01/02/2026". Is that January 2nd or February 1st? Americans and Europeans disagree, and neither format sorts correctly alphabetically.
ISO 8601 solves both problems: YYYY-MM-DDTHH:mm:ss.sssZ
- T separates date from time
- Z at the end means UTC ("Zulu" time)
- Alphabetical sort equals chronological sort —
"2026-12-01" > "2026-01-01" - Unambiguous globally — no region interprets it differently
// Correct: ISO 8601 UTC string
const timestamp = new Date().toISOString();
// "2026-04-04T14:32:00.000Z"
// Store this in your database, pass it in API responses
// Never store "April 4, 2026" or "04/04/2026"
6. Storing Timestamps in Databases
Most databases have a dedicated datetime type — use it. Do not store timestamps as plain integers (Unix epoch in seconds or milliseconds) unless you have a specific reason. Typed columns enable date-range queries, indexing, and timezone conversion at the database layer.
- PostgreSQL: Use
TIMESTAMPTZ(timestamp with timezone). It stores UTC internally and converts on read based on the session timezone. AvoidTIMESTAMP(without timezone) — it stores whatever string you give it, which is a footgun. - MySQL:
DATETIMEstores a literal value with no timezone conversion.TIMESTAMPstores in UTC and converts to the server timezone on read. UseDATETIMEwith UTC values for portability. - MongoDB: Store as ISODate objects, not strings.
- SQLite: Has no native datetime type — store as ISO 8601 text strings in UTC.
7. Leap Seconds (and Why They Do Not Affect You Directly)
The Earth's rotation is gradually slowing due to tidal forces. Occasionally, scientists add a "leap second" to UTC to keep atomic time aligned with astronomical time. Since 1972, 27 leap seconds have been added.
Unix timestamps do not account for leap seconds — they pretend every day is exactly 86,400 seconds. When a leap second occurs (always at 23:59:60 UTC), Unix time stands still for one second. Systems that are not prepared can crash, hang, or fire duplicate scheduled jobs. Notable incidents include a 2012 Linux kernel bug triggered by a leap second that caused widespread CPU spinning in Java applications.
Most modern operating systems and cloud environments handle leap seconds via "smearing" — distributing the extra second over a longer window so clocks never jump. You do not need to handle this manually, but it is why time is ultimately a physical, not purely digital, concept.
Frequently Asked Questions
What timezone should I store in my database?
Always UTC. Store the user's timezone identifier (America/Chicago) separately as
a user preference field if you need to display events in their local time. Never store a
raw offset like -06:00 as a timezone — offsets change with DST, but IANA
identifiers do not.
How do I show a UTC timestamp in the user's local time?
Use the Intl.DateTimeFormat API, which is built into all modern browsers:
function formatForUser(utcISOString, userTimezone) {
return new Intl.DateTimeFormat('en-US', {
timeZone: userTimezone, // e.g. "Europe/London"
dateStyle: 'long',
timeStyle: 'short'
}).format(new Date(utcISOString));
}
formatForUser("2026-04-04T14:00:00Z", "America/New_York");
// "April 4, 2026 at 10:00 AM"
Should I use a date library or the native Date API?
The native Date API and Intl have improved significantly and cover
most use cases. For straightforward timestamp conversion and formatting, they are sufficient.
Use a library like Luxon
or date-fns when you
need: calendar arithmetic across DST boundaries, complex duration calculations, relative
time formatting ("3 hours ago"), or wide compatibility with older browsers. Avoid the original
Moment.js for new projects — it is in maintenance mode and mutable by design.
Need to debug a timestamp right now? Use the Timestamp Converter to convert any Unix timestamp or ISO string to a readable date across any timezone.