When a system serves users within a single geographic region, time zones often do not appear to be a major concern. Developers, users, servers, and business operations may all happen to share the same time zone, making the code seem correct by default.
But once the system starts serving global customers, supporting remote teams across countries, or operating multiple offices and markets, time zones become part of the domain design.
A real-world system may involve:
Customer → Tokyo
Operations team → London
Developer → Vietnam
Office A → New York
Office B → Los Angeles
Server → Singapore
Database → UTCAt that point, the statement:
“The event happens at 9 AM”
is no longer enough.
The real question becomes:
9 AM where?
That is the starting point of nearly every timezone problem.
1. Timezone is part of domain design
A common approach is to treat timezone as only a presentation concern:
Backend stores the date
Frontend formats it using the timezoneThis is only partially correct.
Timezone can directly affect:
Business hours
Calendar dates
Daily reports
Recurring schedules
Notifications
Deadlines
Payment timestamps
User activity
Cron jobs
Analytics
Monthly KPIs
Remote collaboration
For example, suppose a company in New York hosts a webinar:
September 15
10:00 AM
America/New_YorkThe speaker is in New York, while participants may be located in:
London
Tokyo
Vietnam
Sydney
SingaporeThe infrastructure may be running in Singapore.
Everyone sees a different local time, but they are all referring to the same absolute moment.
To design timezone handling correctly, we first need to distinguish three types of temporal data.
Absolute time — a specific moment
Example:
2026-09-15T14:00:00ZThis is an instant.
It represents one exact point on the global timeline.
The same instant may be displayed as:
New York → 10:00
London → 15:00
Vietnam → 21:00
Tokyo → 23:00but the underlying instant does not change.
Fields that usually belong to this category include:
created_at
updated_at
published_at
paid_at
logged_in_at
event_start_at
event_end_at
notification_sent_atIf the question is:
“When did the event actually happen?”
then we are dealing with absolute time.
Local business time — time with local meaning
Suppose an office operates:
Monday
09:00 → 18:0009:00 here is not yet an instant.
It means:
The office opens at 9 AM local time.
Its complete meaning is:
Monday
09:00
America/New_YorkThis recurring business rule should not be converted once to UTC and stored permanently.
For example:
09:00 America/New_Yorkmay correspond to:
13:00 UTCat one time of year, and:
14:00 UTCat another because of DST.
Local business time should therefore remain represented as local wall-clock time + timezone.
Duration — an elapsed amount of time
Examples:
Meeting duration = 90 minutes
Session timeout = 30 minutes
Trial period = 14 daysA duration does not have a timezone.
90 minutesin New York is still 90 minutes in Tokyo.
If a field only represents elapsed time, attaching a timezone to it is unnecessary.
2. UTC and Business Timezone serve different purposes
A useful convention in distributed systems is:
Absolute time should be normalized to UTC in the core system.
Returning to the webinar:
September 15
10:00
America/New_Yorkthe backend may resolve it into:
2026-09-15T14:00:00ZFrom that point onward, the core system can work with that instant:
Database
event_start_at = 2026-09-15T14:00:00Z
Queue
execute_at = 2026-09-15T14:00:00Z
Notification
send_at = 2026-09-15T13:30:00Z
Analytics
timestamp = 2026-09-15T14:00:00ZThe advantage of UTC is that the entire system shares one timeline.
It does not matter:
where the server is running
where the developer is located
where the user is located
which region the worker is running in
An instant remains the same instant.
However, the statement:
“Just store everything in UTC and timezone bugs disappear.”
is not completely correct.
UTC is excellent for answering:
When did the event happen?
But business logic often asks:
Which day does this event belong to?
Those are different questions.
Suppose a transaction occurs at:
2026-09-01T03:30:00ZIn London:
September 1
04:30In Los Angeles:
August 31
20:30If a Los Angeles manager asks:
What was the revenue for August 31?
querying the UTC day:
August 31 00:00 UTC → September 1 00:00 UTCwill not represent the Los Angeles business day correctly.
Instead, the system should resolve:
August 31 00:00 America/Los_Angeles → UTC
September 1 00:00 America/Los_Angeles → UTC
and then query the database using the resulting UTC range.
Mental model:
Business date
↓
Business timezone
↓
Resolve UTC boundaries
↓
Query UTC dataServer timezone is not business timezone
Suppose:
Developer → Vietnam
Server → Singapore
Business → New YorkThis code is fine if the goal is simply to get the current instant:
const now = new Date();But code such as:
const today = dayjs();
const day = today.format('dddd');may be wrong if the real business question is:
What day is it in New York?
For example:
Singapore → Tuesday 01:00
New York → Monday 13:00The server is already on Tuesday, while the business is still on Monday.
If the system selects a working schedule using the server's local date, the business logic may choose the wrong day.
The server timezone should therefore not implicitly participate in domain decisions.
Running infrastructure in UTC is still a good convention:
Application runtime → UTC
Database → UTC
Container → UTC
Scheduler → UTCbut business logic must still use an explicit business timezone.
Browser timezone is not always the source of truth
The frontend can usually detect the user's browser timezone:
Intl.DateTimeFormat().resolvedOptions().timeZoneFor example:
Asia/Ho_Chi_MinhThis is very useful for presentation.
However, the browser timezone is not always the business timezone.
Suppose a manager is traveling in Tokyo while managing an office in London:
Browser timezone → Asia/Tokyo
Business timezone → Europe/LondonIf the dashboard uses the browser timezone to determine:
Today
Yesterday
This week
Current monththe report may become incorrect.
Every UI should therefore answer this question explicitly:
Should this time be displayed using the viewer timezone or the business timezone?
For example:
Messaging applications often prefer viewer timezone.
Operational dashboards often prefer business timezone.
3. Model temporal data according to its semantic meaning
Once instant and local business time are separated, the next step is to model them without losing their meaning.
Use IANA timezones instead of fixed offsets
Avoid representing timezones only as:
UTC+7
UTC-5
GMT+1Prefer IANA timezone identifiers:
Asia/Ho_Chi_Minh
America/New_York
America/Los_Angeles
Europe/London
Asia/TokyoThe biggest reason is DST.
For example:
America/New_Yorkis not always:
UTC-5At some points in the year, it becomes:
UTC-4If the system stores only:
offset = -5it does not contain enough information to determine the timezone behavior in the future.
A useful mental model is:
Offset = the result at a specific moment
Timezone = the rules used to calculate that offsetThe source of truth should therefore be the timezone identifier, not a fixed offset.
Absolute timestamps in the database
With PostgreSQL, absolute timestamps are commonly represented using:
TIMESTAMPTZFor example:
CREATE TABLE webinar_events (
id UUID PRIMARY KEY,
organizer_timezone VARCHAR(100) NOT NULL,
start_at TIMESTAMPTZ NOT NULL,
end_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Conceptually, a record might look like:
organizer_timezone: America/New_York
start_at: 2026-09-15T14:00:00ZAn important detail is that TIMESTAMPTZ does not mean PostgreSQL stores:
America/New_Yorkinside the timestamp.
The timestamp represents an instant.
The timezone carrying business meaning should still be stored separately.
Think of the fields as:
start_at → WHENorganizer_timezone → BUSINESS CONTEXTThey answer different questions.
API contracts should make temporal semantics explicit
Absolute instants should be transferred using ISO-8601 with an explicit UTC indicator or offset.
For example:
{
"startAt": "2026-09-15T14:00:00Z",
"endAt": "2026-09-15T15:30:00Z"
}The frontend may display:
New York → 10:00 AM
Tokyo → 11:00 PMbut the API still transports the same instant.
Avoid values such as:
{
"startAt": "2026-09-15 10:00"
}because the backend cannot determine whether it means:
10:00 New York?
10:00 London?
10:00 Tokyo?
10:00 server time?If the input actually represents a local datetime, make that explicit:
{
"date": "2026-09-15",
"time": "10:00",
"timezone": "America/New_York"
}The backend can then resolve it into an absolute instant.
A local date is not a timestamp
Suppose a user selects:
September 15from a calendar.
This is not yet an instant.
It is simply:
2026-09-15If the developer immediately converts it into:
2026-09-15T00:00:00Zthe semantic meaning has changed.
A local date answers:
Which day on the business calendar?
A timestamp answers:
Which point on the global timeline?
An API can therefore preserve the date as:
{
"date": "2026-09-15"
}and only resolve it when an instant is actually required.
For example:
2026-09-15 + Europe/London + 09:00 -> absolute instant -> UTC4. Recurring schedules and DST
Recurring schedules are especially error-prone when local time is converted to UTC too early.
Suppose a distributed company has a recurring standup:
Every Monday. 09:00. Europe/LondonThe system should not convert it once:
09:00 London → 09:00 UTCand assume that value is permanently correct.
DST may change the UTC representation.
The recurring rule should instead preserve its original semantic meaning:
day_of_week = MONDAY
time = 09:00
timezone = Europe/LondonEach concrete occurrence can then be resolved into UTC.
Mental model:
Recurring rule
↓
Local schedule
↓
Timezone rules
↓
Concrete occurrence
↓
UTC instantThis pattern applies to:
Meetings
Office hours
Scheduled reports
Maintenance windows
Notification campaigns
Remote standups
Market opening times
Why fixed offsets should not be hard-coded
Suppose:
09:00 America/New_Yorkcorresponds at one time to:
14:00 UTCbut at another time to:
13:00 UTCIf the application hard-codes:
09:00 New York = 14:00 UTCthe schedule will shift by one hour when DST changes.
Applications should not maintain DST rules manually.
Instead, their responsibility is primarily to provide:
local datetime + IANA timezoneand allow timezone databases and mature datetime libraries to resolve the applicable rules.
5. Scheduling in global systems
Once an occurrence has been resolved into an instant, scheduling becomes much simpler.
Notifications should be scheduled using absolute time
Suppose an event begins at:
14:00 UTCand a notification should be sent 30 minutes earlier.
The scheduler only needs:
13:30 UTCAt this point, the scheduler does not need to know whether the participant is in Tokyo or New York.
Core logic:
Event instant
↓
minus 30 minutes
↓
Notification instantTimezone conversion happens later when the message is rendered.
A participant in Tokyo might see:
Event starts at 11:00 PMwhile a participant in New York might see:
Event starts at 10:00 AMScheduling and presentation remain separate concerns.
Remote work is also a timezone domain
Timezone handling is not limited to SaaS platforms with global customers.
Remote teams face the same problem.
For example:
Product Owner → San Francisco
Tech Lead → London
Developer → Vietnam
QA → IndiaIf the Product Owner says:
Deploy Monday at 9 AM.
the information is incomplete.
The full meaning should be:
Monday
09:00
America/Los_AngelesOnce confirmed, the schedule can be resolved into UTC.
Each team member may then see:
San Francisco → 09:00
London → 17:00
Vietnam → 00:00 next day
India → 22:30One event.
One instant.
Multiple local representations.
6. Reporting should start from the Business Timezone
Reporting is one of the places where timezone bugs can remain unnoticed for a long time because the results often look “close enough.”
Suppose the business asks for:
Daily active users in Tokyo on July 20.
The system should not directly query:
2026-07-20T00:00:00Z → 2026-07-21T00:00:00ZInstead, it should resolve:
2026-07-20 00:00 Asia/Tokyo → UTC2026-07-21 00:00 Asia/Tokyo → UTCThen query:
WHERE created_at >= :utcStart
AND created_at < :utcEnd
The same pattern applies to:
daily revenue
daily active users
monthly reports
attendance
conversion
transactions
KPIs
Whenever a business question contains concepts such as:
day
week
month
quarterthe system should usually ask:
According to which timezone?
Use half-open intervals for date ranges
Instead of representing a day as:
00:00:00 → 23:59:59prefer:
[start, nextStart)For example:
WHERE created_at >= :startOfDayUtc
AND created_at < :startOfNextDayUtcThis avoids unnecessary concerns about:
milliseconds
microseconds
database precisionIt is a simple but useful convention for reporting and temporal range queries.
7. Coding conventions help prevent timezone bugs
Timezone logic becomes difficult to control when a codebase contains scattered calls such as:
new Date(...)
dayjs(...)
dayjs.utc(...)
.tz(...)
moment(...)
moment.utc(...)with each developer choosing a different conversion strategy.
A better approach is to centralize timezone behavior behind shared abstractions.
For example:
toUtcInstant(...)
toZonedTime(...)
resolveLocalDateTime(...)
getBusinessDayUtcRange(...)
getBusinessLocalDate(...)An internal API might look like:
resolveLocalDateTime({
date: '2026-09-15',
time: '10:00',
timezone: 'America/New_York',
});and return:
2026-09-15T14:00:00ZThe main benefit is not simply reducing a few lines of code.
The real advantage is ensuring that the entire team follows the same timezone conventions.
Naming should preserve temporal semantics
Variables such as:
date
time
start
end
nowcan easily become ambiguous.
More explicit names include:
eventStartUtc
businessTimezone
localBusinessDate
displayTime
utcRangeStart
utcRangeEndWhen a developer sees:
eventStartUtcit is immediately clear that the value represents an instant.
When they see:
localBusinessDateit is clear that the value is not a UTC timestamp.
Many timezone bugs originate when temporal semantics are lost as data moves through different system layers.
Good naming helps preserve that meaning.
8. A practical mental model and team conventions
The overall architecture can be summarized as:
Business / Local Time
↓
Resolve using timezone
↓
Absolute Instant
↓
UTC Core System
↓
Database / Queue / Scheduler / API
↓
Convert for presentation
↓
Viewer / Business TimezoneThe core system has one timeline.
Presentation can have many timezone views.
Before working with any temporal field, ask two questions.
Question 1: Is this an instant or a local time?
If the field represents:
payment happened
event started
message sent
record createdthen it is an absolute instant.
It should generally be normalized to UTC.
If it represents:
office opens at 9 AM
every Monday at 10 AM
September 15 on the calendarthen it represents local business time.
It should not be converted to UTC too early.
Question 2: Who owns the timezone?
A timezone may belong to:
Office
Organization
User
Market
Event
ProjectThe timezone should belong to the domain entity carrying the business meaning.
The following should not accidentally determine business logic:
server timezone
browser timezone
developer timezoneSuggested timezone conventions for a team
A project can start with the following rules:
Absolute datetime → canonical UTC.
Absolute database timestamps →
TIMESTAMPTZ.Absolute datetime in APIs → ISO-8601 with explicit UTC or offset.
Business entities with timezone semantics → store an IANA timezone.
Recurring schedules → store local wall-clock time + timezone.
Calendar dates → preserve
YYYY-MM-DDwhen the semantic is only a date.Duration → no timezone.
Server timezone → UTC.
Browser timezone → presentation context, not business context by default.
Business date ranges → resolve the timezone first, then query UTC.
DST → delegate to timezone databases and libraries.
Timezone conversions → centralize them instead of scattering them across the codebase.
Conclusion
A good timezone architecture is not simply:
Convert everything to UTC.
A better principle is:
Understand what temporal meaning the data carries, which domain object owns the timezone, and at what point that value should be resolved into an absolute instant.
The mental model can be simplified to:
Local business time + IANA timezone
↓
Absolute instant
↓
UTC
↓
Storage / API / Queue / Scheduler
↓
Convert for presentationWhere:
Business timezone → defines the local meaning of time
UTC → provides one consistent timeline for the core system
Display timezone → allows users to view the same instant in the appropriate contextOnce these layers are clearly separated, having customers in Tokyo, operations in London, developers in Vietnam, and infrastructure in Singapore is no longer a special case.
They are simply different local views of the same global timeline.
That is the mental model to use when designing timezone handling in a global system.