Timezone Best Practices for Developers
The essential patterns for handling timezones correctly: storage, conversion, display, and the bugs to watch for.
The Golden Rule
If you remember only one thing from this article, remember this: store timestamps in UTC, convert to local time at the point of display. This single principle prevents the majority of timezone bugs. Your database, API, message queue, log files, and internal representations should all use UTC. The user's local timezone should only appear at the very edge of the system, in the UI layer, where you format the timestamp for human consumption.
This pattern works because UTC is unambiguous, monotonically increasing, and not affected by DST transitions or political timezone changes. Local times are ambiguous (the same local time can occur twice during a fall-back DST transition) and unstable (governments change timezone rules surprisingly often). By centralizing the ambiguity at the display layer, you make the rest of your system simpler and more reliable.
Use IANA Timezone Identifiers
When you need to store or communicate a user's timezone, use the IANA timezone identifier (e.g., America/New_York, Europe/London, Asia/Tokyo). Never use:
- Fixed offsets like UTC-5: These do not account for DST. New York is UTC-5 in winter and UTC-4 in summer.
- Abbreviations like EST or CST: These are ambiguous. CST can mean Central Standard Time (UTC-6), China Standard Time (UTC+8), or Cuba Standard Time (UTC-5). IST can mean India Standard Time, Ireland Standard Time, or Israel Standard Time. Use the Abbreviation Decoder to see how many timezones share the same abbreviation.
- Windows timezone IDs like "Eastern Standard Time": These are Microsoft-specific and do not map cleanly to IANA identifiers.
IANA identifiers encode the full history of timezone changes for a region. The timezone database is updated multiple times per year as governments adjust their rules. By referencing the identifier rather than a fixed offset, your code automatically benefits from these updates.
Database Storage Patterns
PostgreSQL
Use TIMESTAMPTZ (timestamp with time zone), not TIMESTAMP (without time zone). Despite the name, PostgreSQL's TIMESTAMPTZ does not store a timezone. It converts the input to UTC and stores the UTC value. On retrieval, it converts back to the session's timezone. This is exactly the behavior you want.
-- Good: stores UTC internally, converts on display
CREATE TABLE events (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
event_time TIMESTAMPTZ NOT NULL,
user_timezone TEXT NOT NULL -- IANA identifier, e.g. 'America/New_York'
);
-- Query: display in user's timezone
SET timezone = 'America/New_York';
SELECT event_time FROM events;
-- Returns times formatted in Eastern time
-- Bad: TIMESTAMP WITHOUT TIME ZONE
-- Ambiguous: is this UTC? Server local? User local?
event_time TIMESTAMP NOT NULL -- DON'TMySQL
MySQL's DATETIME stores a literal value with no timezone context. TIMESTAMP converts to UTC on storage and back on retrieval (like PostgreSQL's TIMESTAMPTZ). Use TIMESTAMP and store the user's IANA timezone in a separate column.
MongoDB
MongoDB stores Date objects as milliseconds since the Unix epoch (UTC). This is ideal. Store the user's timezone as a string field alongside any date that needs to be displayed in local time.
API Design
Always Use ISO 8601 with Offset
API timestamps should use ISO 8601 format with an explicit UTC offset:
// Good: unambiguous UTC
"created_at": "2026-04-07T14:30:00Z"
// Good: explicit offset
"created_at": "2026-04-07T10:30:00-04:00"
// Bad: no offset, ambiguous
"created_at": "2026-04-07T14:30:00"
// Bad: Unix timestamp with no documentation of precision
"created_at": 1775672400The trailing Z in ISO 8601 means UTC. If you use a numeric offset like -04:00, the server can reconstruct the user's local time. If you use Z, you lose that information but gain simplicity. Both are valid; choose one and be consistent.
Let Clients Specify Their Timezone
If your API returns human-readable dates (event start times, appointment slots), accept a timezone parameter:
GET /api/events?timezone=America/Chicago
Accept-Timezone: America/Chicago // custom header alternativeReturn the raw UTC value alongside the formatted local time. This lets clients that need UTC (other services, cron schedulers) use the raw value, while UIs can display the formatted version.
Frontend Patterns
Detect the User's Timezone
// Modern browsers
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/New_York", "Europe/London", etc.
// Send to server on login or profile setup
await fetch('/api/user/preferences', {
method: 'PATCH',
body: JSON.stringify({ timezone }),
});Always let users override the auto-detected timezone. Some users travel, use VPNs, or prefer to work in a different timezone than their physical location.
Format Dates in the User's Timezone
// JavaScript: Intl.DateTimeFormat handles DST automatically
function formatTime(utcDate, timezone) {
return new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
}).format(new Date(utcDate));
}
formatTime('2026-04-07T18:30:00Z', 'America/New_York');
// "Apr 7, 2026, 02:30 PM EDT"
formatTime('2026-04-07T18:30:00Z', 'Asia/Tokyo');
// "Apr 8, 2026, 03:30 AM JST"Show Relative Timestamps with Absolute Tooltips
For recent events, show relative times ("5 minutes ago", "yesterday") with absolute timestamps in a tooltip. This avoids timezone confusion for recent events while providing precision when needed. Many libraries (date-fns, dayjs, Luxon) provide relative formatting out of the box.
Testing Timezone Code
Timezone bugs are notoriously hard to catch because they only manifest at specific times. Here are strategies for testing:
- Test with multiple timezones: Run your test suite with
TZ=America/New_York,TZ=Asia/Kolkata,TZ=Pacific/Auckland, andTZ=UTC. - Test DST transition dates: Include test cases for the spring-forward and fall-back dates. Test that 2:30 AM on the spring transition is handled, and that 1:30 AM on the fall transition is not ambiguous.
- Test the date boundary: If it is 11 PM in New York, it is tomorrow in London. Ensure your code handles cross-day conversions correctly.
- Test with UTC+14 and UTC-12: These extreme offsets catch assumptions about the maximum offset range and date boundaries.
- Test with half-hour offsets: India (UTC+5:30) and Nepal (UTC+5:45) break code that assumes offsets are always whole hours.
Common Mistakes Checklist
- Storing local times in the database without timezone context
- Using timezone abbreviations (EST, CST) instead of IANA identifiers
- Hardcoding UTC offsets instead of using timezone-aware libraries
- Adding 86,400 seconds instead of 1 calendar day
- Assuming all days have 24 hours
- Assuming the offset between two timezones is constant
- Converting to local time for comparison instead of comparing in UTC
- Scheduling cron jobs at 2:00 AM local time (the DST danger zone)
- Parsing ISO 8601 strings without preserving the offset
- Assuming the user's timezone matches the server's timezone
Try It Yourself
Our Timezone Converter uses the browser's built-in Intl API with IANA timezone identifiers, implementing all the best practices described above. Use it to convert times between zones, plan meetings across distributed teams, or look up IANA identifiers. Pair it with the Epoch Converter to see how Unix timestamps relate to local times in different zones.
Further Reading
- Falsehoods Programmers Believe About Time
The classic and expanded list of incorrect assumptions about time, dates, and timezones.
- Working with Dates and Times — date-fns
date-fns timezone handling documentation with practical patterns.
- PostgreSQL TIMESTAMPTZ
Official PostgreSQL documentation on timestamp with time zone behavior.
- RFC 3339 — Date and Time on the Internet
The standard for internet date/time format (profile of ISO 8601).
- TC39 Temporal Proposal
The next-generation JavaScript date/time API with built-in timezone and calendar support.