Ở bài trước, chúng ta đã xây dựng mental model cơ bản để xử lý timezone trong các ứng dụng global:

Local business time + IANA timezone
        ↓
Absolute instant
        ↓
UTC
        ↓
Storage / API / Queue
        ↓
Convert for presentation

Đây là một foundation tốt.

Tuy nhiên, timezone bugs vẫn có thể xuất hiện trong production ngay cả khi team đã hiểu rằng timestamp nên được chuẩn hóa và lưu dưới UTC.

Lý do là rất nhiều lỗi không nằm ở UTC.

Chúng xuất phát từ những assumption ẩn như:

  • Server đang chạy ở timezone nào?

  • Browser đang ở timezone nào?

  • Giá trị này là local date hay một instant?

  • Report này đang tính theo ngày của ai?

  • DST có làm offset thay đổi không?

  • Scheduler đang chạy theo timezone nào?

Timezone bugs đặc biệt nguy hiểm vì chúng có thể:

  • chạy đúng trong nhiều tháng,

  • chỉ sai ở một số quốc gia,

  • chỉ xuất hiện gần midnight,

  • chỉ xảy ra khi DST chuyển đổi,

  • chỉ sai ở boundary cuối tháng hoặc cuối năm,

  • hoặc chỉ xuất hiện sau khi hệ thống được deploy sang region khác.

Vì vậy, loại bug này rất dễ lọt qua quá trình development và testing thông thường.

Bài viết này tập trung vào những vấn đề timezone phổ biến nhất trong production và cách reasoning chúng một cách có hệ thống.


1. Sai timezone context: Server, Browser và Business

Gom các mục:

  • Dùng server timezone làm business timezone

  • Dùng browser timezone làm source of truth

  • Hard-code một timezone toàn hệ thống

  • Assume tất cả offices cùng timezone

  • Không define rõ ALL timezone

  • User đổi timezone làm historical data thay đổi ý nghĩa

Server timezone không phải business timezone

Giữ nội dung mục 1 hiện tại.

Browser timezone không phải business timezone

Giữ nội dung mục 2.

Có thể kết thúc nhóm bằng mental model:

Server timezone
Viewer timezone
Business timezone
Event timezone

→ là các context khác nhau

Timezone phải là data, không phải assumption

Gom nội dung hard-code timezone + tất cả office cùng timezone + snapshot timezone của event.


2. Sai semantics của Date, Time và API

Gom các mục:

  • Lưu UTC offset thay vì IANA timezone

  • Gửi datetime không có timezone qua API

  • Parse datetime không thống nhất

  • Double conversion

  • Không phân biệt local date và instant

  • Dùng timezone abbreviation làm identifier

  • Không define semantics trong naming

Offset không phải timezone

Giữ ví dụ:

UTC-5 ≠ America/New_York

và giải thích:

Offset = snapshot tại một thời điểm
Timezone = rule để xác định offset theo thời gian

API phải thể hiện rõ datetime semantics

Thay vì:

{
  "startAt": "2026-09-15 10:00"
}

phân biệt:

{
  "startAt": "2026-09-15T14:00:00Z"
}

với:

{
  "date": "2026-09-15",
  "time": "10:00",
  "timezone": "America/New_York"
}

Local date và instant là hai kiểu dữ liệu khác nhau

2026-09-15

không đồng nghĩa với:

2026-09-15T00:00:00Z

Parse và conversion cần convention thống nhất

Gom phần parse datetime + double conversion vào đây.

Rule:

Absolute timestamp → parse UTC / offset-aware
Local datetime     → parse với explicit timezone
Canonical value    → Display value

Không reconstruct instant từ formatted display nếu không cần.

Naming phải thể hiện semantic

Ưu tiên:

eventStartUtc
localBusinessDate
officeTimezone
displayStartTime
utcRangeStart
utcRangeEnd

thay cho:

date
start
end

3. Recurring Schedule và DST

Gom các mục:

  • Convert recurring schedule sang UTC quá sớm

  • Tưởng 24 hours later giống same time tomorrow

  • DST ambiguous time

  • DST nonexistent time

  • Không lưu timezone của recurring rule

Recurring local time không phải fixed UTC time

Ví dụ:

Every Monday
09:00
America/New_York

Không nên convert một lần thành:

Every Monday
14:00 UTC

vì DST có thể khiến offset thay đổi.

Recurring rule nên giữ:

recurrence rule
local time
IANA timezone

và resolve từng occurrence thành UTC instant.

Duration và calendar rule là hai concept khác nhau

24 hours later

không nhất thiết giống:

Tomorrow at 09:00

DST tạo ra local time ambiguous hoặc nonexistent

Khi DST kết thúc, một local time có thể xuất hiện hai lần.

Khi DST bắt đầu, một số local time có thể không tồn tại.

Application cần business policy rõ ràng:

reject?
choose earlier occurrence?
choose later occurrence?
move to next valid time?

Timezone library có thể detect transition, nhưng application phải quyết định behavior.


4. Reporting, Filtering và Date Boundaries

Gom các mục:

  • Query UTC day thay vì business day

  • Dùng 23:59:59 làm cuối ngày

  • Month boundary sai

  • Socket / realtime event bị đưa vào sai ngày hoặc sai tháng

  • Report tổng hợp nhiều timezone mà không định nghĩa semantics

Business day phải được resolve từ business timezone

Nếu cần:

Revenue September 15 của Los Angeles

không nên query trực tiếp:

WHERE created_at >= '2026-09-15T00:00:00Z'
AND created_at <  '2026-09-16T00:00:00Z'

Mà phải:

September 15 00:00 America/Los_Angeles
September 16 00:00 America/Los_Angeles
            ↓
           UTC

sau đó query bằng UTC range.

Rule:

Nếu business question chứa day, week, month, hãy xác định timezone trước khi query.

Dùng half-open interval

Ưu tiên:

[start, nextStart)

Ví dụ:

WHERE created_at >= :start
AND created_at < :nextDayStart

thay vì:

<= 23:59:59

Day/month membership cũng phụ thuộc timezone

Một instant có thể thuộc September theo UTC nhưng vẫn thuộc August tại New York.

Logic này phải giống nhau giữa:

API query
Analytics
Socket filtering
Dashboard

Global report phải định nghĩa timezone semantics

Ví dụ:

Global revenue for March 1

có thể nghĩa:

UTC March 1

hoặc:

Tokyo local March 1
+ London local March 1
+ New York local March 1

Timezone là một phần của metric definition.


5. Scheduler, Queue, Cache và Notification

Gom các mục:

  • Cache key thiếu timezone

  • Cron job chạy theo timezone của host

  • Scheduler và queue dùng local time thay vì instant

  • Notification hiển thị sai timezone

Scheduler không nên phụ thuộc host timezone

0 9 * * *

không tự động có nghĩa:

09:00 business time

Nên dùng:

UTC scheduler

hoặc scheduler hỗ trợ explicit IANA timezone.

Queue nên trao đổi absolute instant

Sau khi business rule đã resolve:

Business rule
     ↓
Resolve timezone
     ↓
UTC instant
     ↓
Queue
     ↓
Worker

Queue payload:

{
  "executeAt": "2026-09-15T14:00:00Z"
}

tốt hơn:

{
  "executeAt": "2026-09-15 09:00"
}

Cache identity phải chứa timezone-dependent context

Nếu timezone ảnh hưởng output:

daily-report:office_123:2026-09-15

hoặc:

daily-report:America/New_York:2026-09-15

Execution timezone và display timezone là hai chuyện khác nhau

Scheduler có thể chạy đúng UTC nhưng notification vẫn có thể hiển thị sai timezone.

Display timezone có thể là:

user timezone
event timezone
organization timezone

tùy domain.


6. Multi-Timezone Domain Design

Gom các mục:

  • User đổi timezone làm historical data thay đổi ý nghĩa

  • Assume tất cả offices cùng timezone

  • Không define rõ ALL timezone

  • Report tổng hợp nhiều timezone không rõ semantics

Có thể giữ phần này ngắn vì một số nội dung đã được giới thiệu ở các nhóm trước.

Ý chính:

Timezone thường thuộc về entity.

Ví dụ:

office.timezone
event.timezone
organization.timezone
user.displayTimezone

Không nên cố ép toàn hệ thống vào:

GLOBAL_TIMEZONE

Một dashboard ALL offices cũng phải định nghĩa rõ:

viewer timezone?
headquarters timezone?
UTC?
aggregate theo local day từng office?

Đây là domain decision, không phải formatting decision.


7. Testing Timezone Correctness

Gom các mục:

  • Chỉ test timezone của developer

  • Không test midnight boundaries

  • Không test month/year boundaries

  • Không test DST transition

Thay vì bốn section riêng, biến thành một timezone test matrix.

Timezone matrix

Ít nhất nên cover:

Asia/Ho_Chi_Minh
America/New_York
America/Los_Angeles
Europe/London
Asia/Tokyo

để có:

positive UTC offset
negative UTC offset
DST
non-DST
large offset differences

Boundary matrix

Test:

23:59
00:00
00:01

và:

January 31 → February 1
February 28/29 → March 1
December 31 → January 1

DST matrix

Test:

day before DST
DST transition
day after DST

cho:

recurring schedules
calendar
notifications
reports
scheduler

8. Architecture và Debugging

Gom các mục:

  • Timezone conversion rải rác khắp codebase

  • Một example bug end-to-end

  • Cách debug timezone bug hiệu quả hơn

  • Timezone bug thường là domain bug, không chỉ date-library bug

Centralize timezone logic

Tránh:

Controller → moment.utc()
Service    → dayjs.tz()
Frontend   → new Date()
Report     → manual offset
Email      → hard-coded timezone

Thay bằng shared utilities:

toUtcInstant()
toZonedTime()
resolveLocalDateTime()
getBusinessDayUtcRange()
getBusinessMonthUtcRange()

Debug theo end-to-end flow

Log đầy đủ:

raw input
input timezone
parsed instant
UTC ISO
business timezone
display output
server timezone

Không chỉ log:

date = 10:00

Một bug end-to-end

Giữ example Los Angeles / Vietnam hiện tại ở đây.

Đây là ví dụ tốt để chứng minh:

Mỗi component có thể đúng theo assumption riêng nhưng toàn bộ system vẫn sai.

Timezone bug thường là semantic bug

Đổi:

Moment → Day.js

hoặc:

Day.js → date-fns

không giải quyết được nếu domain chưa trả lời:

  • timezone thuộc về ai?

  • date này đại diện cho điều gì?

  • Today theo ai?

  • report theo ngày của ai?

  • recurrence theo local clock hay elapsed duration?


9. Production Checklist và Mental Model

Gom:

  • Production timezone checklist

  • Một mental model để review code

  • Kết luận

Code review checklist

Khi gặp một datetime value, hỏi:

  1. Đây là instant hay local time?

  2. Nếu local, timezone thuộc entity nào?

  3. Nếu instant, có canonical UTC không?

  4. Input có đang bị parse bằng implicit timezone không?

  5. Có double conversion không?

  6. Day/month boundary đang theo timezone nào?

  7. Recurring rule là calendar recurrence hay elapsed duration?

  8. Scheduler có phụ thuộc host timezone không?

Production checklist

Giữ checklist hiện tại nhưng không cần một heading ## riêng.

Mental model cuối cùng

Input semantics
      ↓
Timezone context
      ↓
Resolve instant
      ↓
UTC core
      ↓
Database / Queue / Cache
      ↓
Business boundaries
      ↓
Presentation

Nguyên tắc quan trọng nhất:

Đừng hỏi “Date này đang ở timezone nào?” trước tiên. Hãy hỏi “Date này đang đại diện cho điều gì?”