1. The Root Confusion: What a Timezone Actually Is
Most developers treat timezone as a single fixed number — IST is UTC+5:30, done. That model breaks the moment you work with any country that observes Daylight Saving Time (DST), which is roughly 70 countries.
A proper timezone is a named region with rules — not just an offset. The IANA timezone database (what your OS uses) defines ~600 of these: America/New_York, Europe/London, Asia/Kolkata. Each name encodes every offset change in history, including DST transitions that shift forward or back by an hour — sometimes even 30 minutes.
India (Asia/Kolkata, UTC+5:30) does not observe DST, which makes it more predictable than most. But your users, your servers, or your cloud functions might be in places that do. The bug hits the moment you assume "UTC+5:30" means the same offset always, everywhere.
2. The Six Patterns That Cause Timezone Bugs
Pattern 1: Storing Local Time Without Offset
The single most common mistake. You store 2026-06-15 20:00:00 in your database with no timezone context. When your server moves from Mumbai to a Frankfurt data centre (or your CI pipeline runs in US-East), that "8 PM" suddenly means a completely different moment in time.
-- What timezone is this? Nobody knows.
INSERT INTO meetings (title, scheduled_at)
VALUES ('Standup', '2026-06-15 09:00:00');-- Unambiguous. Convert to local only on display.
INSERT INTO meetings (title, scheduled_at)
VALUES ('Standup', '2026-06-15 03:30:00Z');Pattern 2: Using new Date() Without Timezone in JavaScript
new Date() returns the current moment in UTC under the hood — but the moment you call .toLocaleString() or any display method, it uses the browser's local timezone. A user in London during BST (UTC+1) will see a different time than one in IST (UTC+5:30), even if they're looking at the same stored timestamp.
// ❌ This renders in the browser's local TZ — inconsistent
const dt = new Date('2026-06-15T09:00:00');
console.log(dt.toLocaleString()); // Different output per user's OS!
// ✅ Always specify the timezone explicitly
const formatted = dt.toLocaleString('en-IN', {
timeZone: 'Asia/Kolkata',
dateStyle: 'medium',
timeStyle: 'short'
});
// "15 Jun 2026, 2:30 pm" — consistent for everyonePattern 3: Arithmetic on Local Time Strings
Adding 24 hours to a date by adding 86400000 milliseconds mostly works — until you cross a DST boundary in a country that observes it. On that night, one "day" has 23 or 25 hours.
// ❌ Breaks on DST transition days (e.g., US clocks spring forward)
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
// ✅ Use a proper date library that understands calendars
import { addDays } from 'date-fns';
const tomorrow = addDays(today, 1); // Calendar-correctPattern 4: new Date("YYYY-MM-DD") Trap in JavaScript
This one is particularly vicious. The ISO 8601 spec says a date-only string like "2026-06-15" should be treated as UTC midnight. So new Date("2026-06-15") gives you 2026-06-15T00:00:00Z — which is actually June 14th at 6:30 PM in IST.
// ❌ This is actually the 14th at 18:30 IST!
const d = new Date("2026-06-15");
console.log(d.toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' }));
// "14/06/2026, 6:30:00 pm" — not what you expected!
// ✅ Add T00:00:00 to treat as local midnight
const d2 = new Date("2026-06-15T00:00:00");
// Now it's midnight in the local timezonePattern 5: Server and Database in Different Timezones
Your Node.js server is in UTC, your MySQL database is configured in IST (yes, this happens), and your application layer doesn't specify CONVERT_TZ(). Every timestamp inserted is off by exactly 5 hours 30 minutes. You won't notice until an international customer complains.
-- ✅ Always store in UTC. Set explicitly at startup.
SET time_zone = '+00:00';
-- Or check what your current server timezone is:
SELECT @@global.time_zone, @@session.time_zone;Pattern 6: Scheduled Jobs Ignoring DST
Your cron job is set to run at 0 9 * * *. The server is in US/Eastern. In March, when clocks spring forward, that 9 AM job suddenly runs at 10 AM your time — because 9 AM EST became 10 AM EDT. The schedule followed UTC, but your business expected local 9 AM always.
The fix: always specify cron schedules in UTC, convert mentally, and document clearly. 3:30 UTC = 9:00 IST. Pin it in comments.
3. The Golden Rules
4. Recommended Libraries by Language
Native date handling in most languages is painful. These libraries handle the hard parts correctly:
| Language | Library | Key Feature |
|---|---|---|
| JavaScript | date-fns + date-fns-tz | Tree-shakeable, timezone-aware arithmetic |
| JavaScript | Temporal (Stage 3 proposal) | Built-in, proper timezone types |
| Python | pendulum | Timezone-safe by default, great DX |
| Java | java.time (ZonedDateTime) | Built-in since Java 8, use ZoneId |
| Go | time.LoadLocation() | Built-in IANA support |
| Ruby | ActiveSupport TimeWithZone | Rails handles most of this for you |
5. Quick Debug Checklist
When a timezone bug surfaces in production, work through this list:
- What timezone is the database server set to? (
SELECT @@global.time_zone) - What timezone is the application server set to? (
datein terminal) - Are timestamps stored with or without timezone context?
- Where is the timezone conversion happening — database, server, or frontend?
- Does the affected timestamp cross a DST boundary in any relevant timezone?
- Is the issue in storage, retrieval, or display? Isolate each layer.
In Node.js, run new Date().toISOString(). It should end in Z (UTC). If it shows a +05:30 offset, your runtime isn't in UTC and timestamps will drift from your database.
Timezone Converter Tool
Convert times between any timezone instantly — IST, UTC, EST, PST, and 500+ more IANA zones. Great for scheduling global meetings or validating your UTC conversions.
Open Timezone Converter