Ở 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õ
ALLtimezoneUser đổ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 nhauTimezone 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_Yorkvà giải thích:
Offset = snapshot tại một thời điểm
Timezone = rule để xác định offset theo thời gianAPI 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-15không đồng nghĩa với:
2026-09-15T00:00:00ZParse 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 valueKhô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
utcRangeEndthay cho:
date
start
end3. Recurring Schedule và DST
Gom các mục:
Convert recurring schedule sang UTC quá sớm
Tưởng
24 hours latergiốngsame time tomorrowDST 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_YorkKhông nên convert một lần thành:
Every Monday
14:00 UTCvì DST có thể khiến offset thay đổi.
Recurring rule nên giữ:
recurrence rule
local time
IANA timezonevà resolve từng occurrence thành UTC instant.
Duration và calendar rule là hai concept khác nhau
24 hours laterkhông nhất thiết giống:
Tomorrow at 09:00DST 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:59làm cuối ngàyMonth 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 Angeleskhô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
↓
UTCsau đó 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 < :nextDayStartthay vì:
<= 23:59:59Day/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
DashboardGlobal report phải định nghĩa timezone semantics
Ví dụ:
Global revenue for March 1có thể nghĩa:
UTC March 1hoặc:
Tokyo local March 1
+ London local March 1
+ New York local March 1Timezone 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 timeNên dùng:
UTC schedulerhoặ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
↓
WorkerQueue 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-15hoặc:
daily-report:America/New_York:2026-09-15Execution 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 timezonetù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õ
ALLtimezoneReport 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.displayTimezoneKhông nên cố ép toàn hệ thống vào:
GLOBAL_TIMEZONEMộ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 differencesBoundary matrix
Test:
23:59
00:00
00:01và:
January 31 → February 1
February 28/29 → March 1
December 31 → January 1DST matrix
Test:
day before DST
DST transition
day after DSTcho:
recurring schedules
calendar
notifications
reports
scheduler8. 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 timezoneThay 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 timezoneKhông chỉ log:
date = 10:00Mộ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.jshoặc:
Day.js → date-fnskhô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:
Đây là instant hay local time?
Nếu local, timezone thuộc entity nào?
Nếu instant, có canonical UTC không?
Input có đang bị parse bằng implicit timezone không?
Có double conversion không?
Day/month boundary đang theo timezone nào?
Recurring rule là calendar recurrence hay elapsed duration?
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ì?”