Unix timestamp 2038 và giờ Việt Nam (UTC+7) | int32, int64, JWT exp
Epoch là giây (hoặc milli) kể từ 1970-01-01 UTC — không chứa múi giờ. Giờ Việt Nam = UTC+7 chỉ khi hiển thị. Vấn đề 2038 = trần int32 signed (2_147_483_647). Trước khi lưu expiry / JWT exp: chọn BIGINT/int64 hoặc timestamptz, thống nhất giây vs milli, đọc nhanh bằng Unix Timestamp.
JWT exp: 2147483647 trông “xa”. Đổi sang ngày: 2038-01-19 03:14:07 UTC — tức 10:14 sáng 19/01/2038 giờ Hà Nội. Đó cũng là max int32. Cột MySQL INT lưu exp sẽ không chứa được hạn sau mốc này; firmware IoT cũ và một số serializer nhị phân vẫn sống trong thế giới 32-bit.
Bài này cho backend / freelancer VN hay debug log epoch, JWT, và DB legacy — kết hợp 2038, UTC+7, và checklist trước khi ship.
Bài viết này giúp bạn
- Hiểu Unix timestamp và vì sao không “mang” +7 bên trong
- Đọc 2038 đúng (int32 vs int64)
- Phân giây / milli
- Checklist lưu expiry / JWT exp
- Dùng Unix Timestamp Kawa để soi số thành giờ VN
Unix timestamp là gì
Một số nguyên: số giây (hoặc milli) đã trôi từ:
1970-01-01 00:00:00 UTC
Cùng một khoảnh khắc → cùng một số trên mọi máy (bỏ qua lệch đồng hồ). Chuỗi "19/07/2026 17:00" thì phụ thuộc format và TZ — epoch thì không.
const sec = Math.floor(Date.now() / 1000); // giây
const ms = Date.now(); // milli — JS hay dùng
Dán số vào công cụ chuyển timestamp khi log chỉ toàn 1719….
Giờ Việt Nam (UTC+7) — đừng nhét vào epoch
Việt Nam không dùng DST: UTC+7 quanh năm (Asia/Ho_Chi_Minh).
| Cách đúng | Cách sai |
|---|---|
| Lưu / truyền epoch UTC | Cộng 7*3600 vào epoch rồi lưu “cho dễ” |
Format UI: Intl / lib với timeZone: 'Asia/Ho_Chi_Minh' | Giả sử server Node “đã là giờ VN” nên trừ bớt 7 khi ghi |
| Log ghi cả UTC và VN khi điều tra | So sánh JWT exp (UTC) với new Date() local mà quên offset |
Ví dụ đọc exp:
exp (giây) = 1721376000
UTC ≈ 2024-07-19 08:00:00
VN (UTC+7) ≈ 2024-07-19 15:00:00
Nhầm “số epoch đã là giờ VN” → lệch đúng 7 giờ khi đối chiếu với khách ở Hà Nội — triệu chứng kinh điển ticket “token hết hạn sớm”.
Vấn đề năm 2038
Cơ chế
Signed int32 max = 2_147_483_647.
2_147_483_647 giây sau epoch
= 2038-01-19 03:14:07 UTC
= 2038-01-19 10:14:07 giờ VN
Giây tiếp theo không biểu diễn được trong int32 → wrap (thường về âm ≈ năm 1901) tùy ngôn ngữ / CPU.
int32 vs int64
| Kiểu | Phạm vi thực tế | Gợi ý |
|---|---|---|
| int32 / INT | Tới ~2038-01-19 | Tránh cho expiry dài, lease, JWT dài hạn |
| int64 / BIGINT | Xa vượt tuổi hệ mặt trời | Chuẩn cho epoch giây hiện đại |
| timestamptz | Kiểu thời gian DB | Tốt nếu ORM/SQL — vẫn cẩn thận driver map sang int32 |
Nơi vẫn gặp int32:
- Schema MySQL cũ:
created_at INT - Protobuf / struct C
int32 time - Một số hệ nhúng, thiết bị cửa, đồng hồ chấm công
- “Tối ưu” lưu 4 byte khi đĩa đắt — năm 2010
Hệ 64-bit userland (Node, Go, JVM hiện đại) tính toán thường an toàn; cột DB và wire format mới là chỗ chết.
Giây vs milli — bảng nhận diện
| Độ dài gần đúng | Đơn vị | Ví dụ nguồn |
|---|---|---|
| ~10 chữ số | giây | JWT exp, PHP time(), nhiều API Unix |
| ~13 chữ số | milli | Date.now(), Java Instant.toEpochMilli() |
| ~16 chữ số | micro (hiếm hơn) | Một số DB / telemetry |
// SAI — coi giây như milli
new Date(1721376000); // ~1970 (sai đơn vị)
// ĐÚNG
new Date(1721376000 * 1000); // giây → Date
Checklist so sánh:
nowSec = Math.floor(Date.now() / 1000)
if (payload.exp < nowSec) token hết hạn
Đừng so exp (giây) với Date.now() (milli).
Checklist trước khi lưu expiry / JWT exp
In ra và tick trước merge:
- Đơn vị — giây hay milli? Ghi trong comment / OpenAPI.
- Kiểu cột —
INT? ĐổiBIGINThoặcTIMESTAMPTZnếu hạn có thể ≥ 2038. - Giá trị mẫu — dán vào Unix Timestamp, đọc UTC và VN (+7) có khớp product spec không (ví dụ “hết hạn cuối ngày 31/12/2030 giờ VN”).
- So sánh — cùng đơn vị với
now. - Không nhân đôi +7 — epoch UTC; offset chỉ lúc format.
- JWT —
explà NumericDate (giây). Đừng nhét milli vàoexp. - Legacy import — migrate INT → BIGINT trước khi insert hạn sau 2038; test với
2147483647và2147483648.
-- Nguy hiểm nếu còn INT
ALTER TABLE sessions MODIFY exp_at BIGINT NOT NULL; -- ví dụ migrate
(Chỉnh theo dialect; ý tưởng: thoát int32.)
Case study
1. Freelancer nhận Laravel cũ
password_resets.expires kiểu INT. Product muốn link reset “không hết hạn trong 5 năm” từ 2036 → cận 2038. Insert fail hoặc wrap. Fix: BIGINT + test biên.
2. Nest JWT “hết hạn lúc 10h sáng dù set midnight”
Team set “midnight VN” bằng cách lấy Date local server (UTC trên VPS) rồi /1000. User VN thấy lệch. Fix: tính midnight theo Asia/Ho_Chi_Minh rồi lấy epoch UTC — không cộng tay 7*3600 lung tung.
3. Log 1710000000000 bị đọc như giây
Tool/UI nhân thêm 1000 → năm cực đại. Nhìn 13 số → milli; đừng nhân.
Cách dùng tool trong điều tra
- Copy số từ JWT / log / DB
- Dán Unix Timestamp
- Xác nhận giây vs milli
- Đọc giờ VN (+7) so với ticket khách
- Nếu gần
2147483647→ cảnh báo int32 / 2038
Tóm tắt
| Chủ đề | Nhớ một câu |
|---|---|
| Epoch | Đếm từ UTC 1970, không chứa +7 |
| VN | +7 chỉ khi hiển thị / schedule local |
| 2038 | Trần int32 giây |
| JWT exp | Giây; đừng nhầm milli |
| Lưu trữ | BIGINT / timestamptz cho hạn xa |
Số epoch không đáng sợ — đáng sợ là đơn vị sai, TZ cộng tay, và cột 32-bit im lặng tới 2038.