Bug timezone & DST yang memanggang produksi
Isi
Timezone bug bukan bug logika. Bug ini bug asumsi. Kode benar, tes hijau, lalu Senin pagi laporan masuk: "data tanggal 27 hilang", "job jalan dua kali", "invoice kemarin masuk ke hari ini".
Tiga kelas bug yang paling sering menghantam produksi:
1. Nyimpen waktu lokal tanpa offset
-- salah
created_at DATETIME -- "2026-10-23 14:00" — 14:00 di mana?
String itu tidak bisa diurutkan lintas region, tidak bisa dikonversi, dan jadi ambigu dua kali setahun saat DST fall-back (jam 01:30 terjadi dua kali).
Aturan main:
- Simpan UTC. Selalu.
- Render di edge (browser/user timezone), bukan di DB.
- Postgres:
timestamptz(simpan UTC, render sesuaiTimeZonesession).timestamptanpa tz = wall clock telanjang — pakai hanya untuk hal yang memang bukan instant. - MySQL:
TIMESTAMPconvert ke UTC,DATETIMEtidak. Bedanya nyata, bukan kosmetik. - Python:
datetime.now(timezone.utc), jangandatetime.now(). Naive datetime adalah bug yang menunggu jadwal.
Pengecualian penting: waktu yang "bermakna lokal" — jadwal meeting, reminder harian jam 9 pagi, jam buka toko. Itu bukan instant, itu intent. Simpan UTC plus nama timezone IANA (Asia/Jakarta), bukan offset +07:00. Offset bisa berubah (kebijakan pemerintah), nama timezone mengikuti database tz.
2. Asumsi "1 hari = 24 jam"
Di zona ber-DST, satu hari bisa 23 atau 25 jam.
# rapuh
besok = sekarang + timedelta(days=1)
# benar kalau maksudnya "tanggal besok, jam sama"
besok = (sekarang.astimezone(tz) + timedelta(days=1)).replace(hour=9)
Konsekuensi nyata: laporan "penjualan 24 jam terakhir" bolong 1 jam atau dobel 1 jam saat transisi. Billing cycle yang dihitung +30*24*3600 detik bisa geser satu jam dan bikin tagihan beda tipis — cukup buat tim finance buka tiket.
3. Cron job di jam yang tidak ada
Spring forward: jam 02:00–02:59 tidak pernah terjadi. Job 30 2 * * * dilewati. Fall back: jam 01:00–01:59 terjadi dua kali. Job jalan dua kali.
Perilaku cron beda-beda antar implementasi (Vixie cron, systemd timer, Kubernetes CronJob, cloud scheduler) — sebagian skip, sebagian jalan sekali di waktu pertama, sebagian dua kali. Jangan diasumsikan. Dites.
Pola aman: jangan jadwalkan job kritis di 00:00–03:00 lokal. Pilih 04:00. Atau jadwalkan dalam UTC dan buat job-nya idempoten (kunci berbasis tanggal bisnis, bukan timestamp eksekusi).
Kasus dunia nyata yang bikin geleng kepala
- Brasil menghapus DST pada 2019 (Decree 9.772/2019). Sistem yang hardcode
-03:00/-02:00langsung salah satu jam. - Turki pindah permanen ke UTC+3 pada September 2016 — offset berubah, tz database berubah, aplikasi yang simpan offset lama jadi meleset.
- Nepal UTC+5:45. India +5:30. Lord Howe Island DST-nya cuma 30 menit. Rumus "offset selalu kelipatan 1 jam" = salah.
- Indonesia aman: WIB/WITA/WIT, tanpa DST. Tapi begitu user-nya ada di Singapura, Jepang, atau server-nya di
us-east-1, masalahnya muncul. Server di UTC, user di mana-mana — itu default, bukan edge case.
Bau-bau kode yang harus dicurigai
new Date('2026-10-23')di JavaScript → diparse sebagai UTC midnight (date-only string = UTC per spec). Sementaranew Date('2026-10-23T00:00')→ local. Beda satu karakter, beda satu hari untuk user di UTC-5. Ini penyebab nomor satu "tanggal lahir geser sehari".toISOString()untuk ditampilkan ke user → selalu UTC. User Jakarta lihat jam 07:00 sebagai "00:00".- Hardcode
+07:00di mana pun. - Perbandingan tanggal pakai
substring(0,10)dari timestamp UTC. - Ulang tahun / anniversary disimpan sebagai timestamp. Itu tanggal, bukan instant — pakai tipe
DATE.
Satu hal yang bisa langsung dipakai
Audit cepat, 5 menit, di repo mana pun:
# cari kandidat bahaya
rg -n "datetime\.now\(\)|new Date\(\)|getTimezoneOffset|toISOString\(\)|\+07:00|3600 \* 24|86400"
Lalu cek satu pertanyaan untuk tiap hasil: "nilai ini instant atau wall clock?" Kalau instant → harus UTC. Kalau wall clock → harus bawa nama timezone.
Bonus tes murah: jalankan test suite dengan TZ=America/Santiago dan TZ=Asia/Jakarta. Banyak bug timezone langsung muncul tanpa perlu nulis tes baru. Satu baris, hasil instan.
Takeaway
UTC untuk menyimpan, timezone name untuk bermakna, dan jangan pernah percaya bahwa satu hari itu 24 jam.
Kalau cuma ingat satu hal: offset adalah fakta yang bisa berubah oleh keputusan politik — nama timezone adalah satu-satunya yang tahan.