A nightly job has run correctly for months. One morning in spring the numbers are wrong by an hour, and a week later they are fine again. Nothing was deployed.
This is the most common date bug there is, and understanding it takes one idea: a timestamp does not have a time zone.
What a timestamp is
A Unix timestamp is a single integer: the number of seconds elapsed since 1 January 1970 at 00:00:00 UTC. That is the whole definition.
1789200000 is a specific instant. It is the same instant in Mumbai, London and São Paulo. There is no zone stored in it, because none is needed. A zone is only involved when you turn that number into something a human reads, and a different zone gives a different reading of the same instant.
So the timestamp is the fact and the displayed date is an interpretation. Bugs happen when interpretation is mistaken for the fact.
Offsets are not time zones
+05:30 is an offset, a fixed number of minutes from UTC.
Asia/Kolkata is a time zone, which is a set of rules describing which offset applies on which dates, including historical changes and daylight saving transitions.
The difference is the source of the spring bug. Europe/London is +00:00 in winter and +01:00 in summer. If you stored an offset instead of a zone, you recorded the answer for one instant and have no way to work out the answer for another.
This is why "store UTC, display local" is the standard advice. Storing the instant keeps the fact. Applying a zone at display time re-runs the rules for that particular date and gets it right in both halves of the year.
The transitions that actually break things
Twice a year, local time does something a naive calculation does not expect.
Spring forward. In the US, for example, clocks jump from 01:59 to 03:00. The hour between does not exist. A job scheduled at 02:30 local either does not run or runs twice, depending on the scheduler. A date arithmetic routine that adds 24 hours to a timestamp and calls it "tomorrow" lands an hour off.
Autumn back. Clocks go from 02:59 back to 02:00. That hour happens twice, so 02:30 local is ambiguous and maps to two distinct instants. A log sorted by local time appears to go backwards.
The rule that avoids both: do arithmetic in UTC, convert for display. "Add one day" is not "add 86,400 seconds" if a transition falls in between, and only the zone rules know that.
Comparing across zones
When the question is "what time is the call for everyone", the answer is not arithmetic on offsets, because the offsets on the day of the meeting may not be the offsets today.
A time zone converter that works from zone names rather than fixed offsets applies the rules for the actual date, which is why it can tell you that the London to New York gap is five hours for most of the year but four for about three weeks in March and one week around the start of November, because the US and UK change their clocks on different dates. That gap exists, it catches recurring meetings every spring, and no amount of offset arithmetic predicts it.
Date arithmetic is not subtraction
Working out an age or a duration in whole years and months is not a division problem, because months have different lengths and years have different numbers of days.
Subtracting two timestamps and dividing by 31,536,000 gives an answer that drifts, because it ignores leap years. Someone born on 29 February has no birthday in most years, and different jurisdictions answer the legal question differently.
The correct approach is calendar arithmetic: compare year, then month, then day, borrowing where needed. That is what an age calculator does, and why its answer differs from a spreadsheet formula that divides by 365.25.
Practical rules
- Store instants in UTC. Seconds or milliseconds, pick one and be consistent.
- Store the zone name, not the offset, when you need to know where something happened.
- Do arithmetic in UTC, convert at the edges.
- Use ISO 8601 in files and APIs.
2026-09-13T14:30:00Zis unambiguous and sorts correctly as plain text. - Test around the transitions. A test suite that only runs in one half of the year will not find this class of bug.
When a date looks wrong, check the raw value first in the timestamp converter, then compare zones for the actual date with the time zone converter; for ages and durations in calendar terms, the age calculator does the month-and-day arithmetic.