CURRENT EPOCH · EPOCHTIME.TOOLS · A PRECISION INSTRUMENT FOR TIME
Converter Batch Difference Blog
Languages
JavaScript Python TypeScript Go Rust Java PHP SQL Bash
Specialty
LDAP Timestamp .NET Ticks Chrome/WebKit Cocoa / Core Data Discord Timestamp Excel OADate Unix Hex
Standards
ISO 8601 Guide Year 2038 NTP Timestamp GPS Time Julian Day

The pattern

"The dates are off by a few hours." That's the bug report. It's never specific. Sometimes it's exactly the timezone offset, sometimes it's wrong by daylight saving, sometimes it's a day off and you can't figure out why. Here are five real instances I've debugged, the symptoms that gave each one away, and what the fix looked like.

Case 1: The date-only string that became UTC midnight

Symptom: Users in Australia were saving events with the right date in the UI, but the database showed events dated the day before. Users in the UK never noticed.

The code:

// Front-end sent the date as "2025-03-15"
const dateFromInput = "2025-03-15";
const ts = new Date(dateFromInput);
// ts.toISOString() === "2025-03-15T00:00:00.000Z"

What's happening: The ISO 8601 spec says date-only strings without a timezone are parsed as UTC midnight. JavaScript follows this. So new Date("2025-03-15") in Sydney represents 11 AM on March 15 local time — but the user typed "March 15" meaning the whole day in their timezone. When you store the timestamp and convert back to local time anywhere east of UTC, the date silently becomes "March 14" because midnight UTC is still the previous day in Sydney.

The fix: if the user gave you a date, store it as a date string and don't convert to a timestamp at all. Or, if you need a timestamp, anchor it to the user's timezone explicitly:

// Use the user's timezone to construct the moment
const userTz = "Australia/Sydney";
const date = "2025-03-15";
// Parse as local-to-userTz instead of UTC
const localMidnight = new Date(date + "T00:00:00");  // local
// Better: keep dates as strings, don't convert to timestamps

Case 2: The recurring meeting that shifted by 1 hour every March and November

Symptom: A weekly standup scheduled at "9:00 AM PT every Wednesday" was showing up at 8 AM or 10 AM for users in Europe and Asia, but only twice a year, for a week or two at a time.

The code:

// The meeting was stored with a UTC offset, not an IANA timezone
const meeting = {
  time: "09:00",
  offset: "-08:00"  // Pacific Standard Time
};

What's happening: The team in California changed to Pacific Daylight Time on March 9. But the stored offset stayed at -08:00 (PST, not PDT). So for one Wednesday after that switch, the meeting was at "9 AM PST" — which is now 10 AM real-time-for-everyone-else, because California had moved forward. Same thing happens in reverse in November when DST ends in the US but Europe hasn't switched yet.

The fix: never store fixed UTC offsets for anything that recurs. Always use IANA timezone names:

const meeting = {
  time: "09:00",
  timezone: "America/Los_Angeles"  // handles DST automatically
};

Then when you compute the next occurrence, use a timezone-aware library (Luxon, date-fns-tz, Temporal) that knows when DST happens in America/Los_Angeles.

Case 3: The log timestamps that drifted by 50 minutes for some users

Symptom: Logs from the Nepal office were dated 50 minutes off from server time. India was off by 30. Iran was off by 30. Customers complained that their account activity timeline showed events at impossible times.

What's happening: The code was using (server_time + user_utc_offset_minutes), and the offset was being stored as signed integers in 15-minute increments. Nepal's actual offset is +5:45, India is +5:30, Iran is +3:30 — none of which round to 15-minute increments. The code was forcing everything to multiples of 15 minutes, which silently introduced errors for any timezone with a 30 or 45-minute component.

This is a surprisingly large number of timezones: India (+5:30), Sri Lanka (+5:30), Iran (+3:30), Afghanistan (+4:30), Myanmar (+6:30), Nepal (+5:45), Chatham Islands (+12:45), parts of Australia (+8:45, +9:30, +10:30), and more.

The fix: store offsets in minutes, not 15-minute units. Better yet: store IANA timezone names and let the library compute the offset.

Case 4: The order export that lost a day every year on the last day of December

Symptom: An annual sales report grouped orders by year. Every January, the accountant would notice that one or two orders from December 31 had been counted in the next year.

The code:

SELECT YEAR(order_timestamp_utc) AS year, SUM(total)
FROM orders
GROUP BY YEAR(order_timestamp_utc);

What's happening: Order timestamps were stored in UTC. The reporting team was in New York (UTC-5). An order placed at 8 PM EST on December 31 has a UTC timestamp of 1 AM January 1. The accountant sees "Dec 31 8 PM" in the UI (correctly converted to local for display), but the SQL query is grouping by UTC year and putting that order in the next year's totals.

The fix: convert to local time in the query, then extract the year:

-- PostgreSQL
SELECT EXTRACT(YEAR FROM order_timestamp_utc AT TIME ZONE 'America/New_York') AS year,
       SUM(total)
FROM orders
GROUP BY year;

Or, if your business has a "fiscal year" or "reporting timezone," store that as a config value and use it consistently in every query.

Case 5: The mobile app that showed the right local time everywhere — except for one user on a transatlantic flight

Symptom: A user in seat 14C reported that their app's calendar showed everything one hour wrong, but only sometimes. When they got off the plane, it was fine again.

What's happening: The app cached the user's timezone at login. When they boarded a flight from London to New York, they crossed the Atlantic, their phone updated to the new timezone, but the app's cached userTz = "Europe/London" didn't refresh. The phone said New York time, the app said London time, mid-flight events showed up at impossible offsets.

The fix: never cache a user's timezone for longer than a session. Re-read it on every app launch, ideally on every meaningful screen transition:

// Read the current timezone live, don't cache it
const currentTz = Intl.DateTimeFormat().resolvedOptions().timeZone;

For server-side code, ask the client for the timezone on every request that displays times. The cost is negligible; the bug-prevention is real.

The pattern across all five

If you look at all five, the common thread is: timezone information was treated as a static value rather than a function of context. Fixed offsets instead of IANA names. Cached values that didn't update. Conversions that happened at the wrong point in the pipeline.

The boring advice: always store moments in UTC, always store timezones as IANA names, always convert at the display layer using the user's current timezone, and never put timezone math inside a query unless you're willing to think hard about every possible edge.

You can sanity-check any timezone conversion live with the main converter — paste in the timestamp and switch the IANA dropdown to confirm which local moment it represents. If your code disagrees with the tool, the tool is probably right.


Published May 8, 2026. Tagged: timezones, debugging, production.

← Back to blog  ·  Try the converter