The Golden Rule: Store UTC, Display Local
The most important rule in programming with time is simple: always store timestamps in UTC, and convert to the user's local timezone only when displaying. This single principle prevents the vast majority of timezone-related bugs in software.
Why UTC?
- Unambiguous: UTC has no daylight saving time, no political changes, no offsets. A UTC timestamp always means the same moment in time.
- Comparable: Two UTC timestamps can be compared and sorted without conversion.
- Portable: Your database, your server logs, and your API all agree on UTC regardless of where they are deployed.
- Future-proof: Governments change timezone rules. Storing UTC means your historical data remains correct even if a country shifts its offset.
Common Mistakes
# BAD: Storing local time
created_at = datetime.now() # naive datetime — timezone unknown!
# GOOD: Storing UTC
from datetime import datetime, timezone
created_at = datetime.now(tz=timezone.utc) # timezone-aware UTC
# BAD: Converting before storage
user_time = datetime(2024, 3, 1, 14, 0, tzinfo=some_local_tz)
db.save(user_time) # storing local time — dangerous
# GOOD: Convert to UTC before storage
utc_time = user_time.astimezone(timezone.utc)
db.save(utc_time)
Database Configuration
Ensure your database server and application server are both set to UTC:
# PostgreSQL — set default timezone
SET timezone = 'UTC';
-- in postgresql.conf: timezone = 'UTC'
# MySQL
SET time_zone = '+00:00';
# Django settings
USE_TZ = True # always True in modern Django
TIME_ZONE = 'UTC' # server timezone
API Design: Always Include Timezone Information
When returning dates in APIs, always include timezone information in the response:
# BAD
{"created_at": "2024-03-01 14:00:00"}
# GOOD — ISO 8601 with UTC offset
{"created_at": "2024-03-01T14:00:00Z"}
{"created_at": "2024-03-01T14:00:00+00:00"}
Displaying Local Time to Users
Convert from UTC to local time as late as possible — ideally in the view layer or on the client side:
# Python — convert UTC to user's timezone for display
from zoneinfo import ZoneInfo
utc_dt = datetime(2024, 3, 1, 14, 0, tzinfo=timezone.utc)
seoul_dt = utc_dt.astimezone(ZoneInfo("Asia/Seoul"))
print(seoul_dt) # 2024-03-01 23:00:00+09:00
# JavaScript — browser handles local conversion
new Date("2024-03-01T14:00:00Z").toLocaleString("ko-KR", {
timeZone: "Asia/Seoul"
})
Logging
Always log in UTC. Mixing timezones in log files makes debugging across servers in different regions extremely difficult. Include the timezone indicator explicitly: 2024-03-01T14:00:00Z — the trailing Z means UTC.