Time Zone Rules Every Developer Should Follow
Time zone bugs are some of the nastiest in software. They appear only under specific conditions: DST transitions, international users, midnight boundaries, or date-line crossings. A feature that works perfectly for a year can break on a single day in March.
Rule 1: Store Everything in UTC
This is the cardinal rule, and yet production databases are full of local timestamps without zone information. Every time you store a timestamp without a time zone, you are creating a bug that will surface exactly once — when a user in a different time zone reads the data and gets the wrong answer.
Store UTC. Always. Without exception. Convert to local time only at the last possible moment — when displaying to a user.
Rule 2: Never Trust the Client's Clock
The user's system clock is not authoritative. Users have wrong time zones set, dead batteries reset their clock to 2001, and JavaScript's Date object has been known to lie about DST transitions. If your application logic depends on "what time is it," ask your server, not the browser.
For recurring scheduled events, store the user's intended local time and their time zone (as an IANA zone name, like "America/New_York", not "EST"). Store intent, compute result.
Rule 3: Use Proper Time Zone Libraries
Rolling your own time zone logic is one of the most common sources of production bugs. Use:
- Python: zoneinfo (stdlib since 3.9) or pytz 2024b+
- JavaScript/TypeScript: Temporal (stage 3, use polyfill) or date-fns-tz
- Java: java.time (Java 8+)
- C#/.NET: TimeZoneInfo with IANA mapping via Noda Time
- Go: time.LoadLocation from the standard library
- Databases: TIMESTAMPTZ in PostgreSQL, DATETIMEOFFSET in SQL Server
Avoid abbreviations like "EST," "PST," "CST." They are ambiguous (CST can mean China Standard Time or Central Standard Time) and do not encode DST rules.
Rule 4: Test at Boundaries
Most time zone bugs appear at transitions: DST start, DST end, midnight, year boundaries, and the International Date Line. Test these explicitly:
- The 23-hour DST "spring forward" day
- The 25-hour DST "fall back" day (including the ambiguous hour)
- Midnight in every target time zone
- Date line crossings
- Leap year February 29
Rule 5: Disambiguate the "Fall Back" Hour
When clocks roll back, there are two 1:30 AMs. Any scheduling system must disambiguate: "the first 1:30 AM" vs "the second 1:30 AM." Most systems fail silently here.
Best practice: ask users to pick a future time in UTC, or to confirm which occurrence if the local time falls in an ambiguous window.
Real-World Examples of Time Zone Disasters
Calendar apps showing wrong meeting times: The classic. User schedules a recurring meeting for 2 PM London time, but the app stores 2 PM UTC. Now it shows 3 PM (1 PM in winter). Fixes itself six months later when DST flips. Nobody trusts the app.
Booking systems charging the wrong day: A hotel charges per night. Guest books "March 15 to March 18." If the checkout time converts to March 17 at 11 PM UTC but renders as March 18 local time, that is a free night nobody intended.
Medical record timestamps: In healthcare, a timestamp without zone information can cause misdiagnosis. The doctor sees "medication given at 8 AM" — was that 8 AM in the hospital's time zone or the patient's home time zone?
FAQ
Should I store time zone offsets or IANA zone names?
Store IANA zone names (e.g., "America/New_York"), not offsets (e.g., "-05:00"). Offsets change with DST; zone names encode the full rule history.
How do I handle user availability across DST?
Store "9 AM local time in America/New_York" — not the UTC equivalent. When DST changes, 9 AM local is automatically 13:00 or 14:00 UTC as appropriate.
What about time zones that change their rules mid-year?
Use a library that updates with the IANA timezone database releases. These happen several times a year. Subscribe to the tz-announce mailing list.