Search tools

Find a tool by name or what it does.

Time guides

Timestamps and time zones: the mistakes that cost you a day

A Unix timestamp has no time zone, an offset is not a time zone, and one missing factor of a thousand will put your date in 1970. Here is the small set of rules that survives contact with reality.

6 min read

On this page

A timestamp is a moment, not a reading on a clock

A Unix timestamp is a single number: the count of seconds elapsed since midnight UTC on 1 January 1970. That is the whole definition. 1750000000 is one specific instant in the history of the universe, and everyone alive experienced it simultaneously.

This is why people who say "convert this timestamp to UTC" have already gone slightly wrong. The number is not in a time zone waiting to be moved out of one. It is time zone free by construction. Time zones only appear at the moment you decide to render it as words a human can read, and the same number renders as 15:00 in London and 10:00 in New York without either being more correct.

The mental model that fixes almost everything: a timestamp is a point, a wall clock reading is a description of that point from somewhere. Points are unambiguous. Descriptions are not.

TL;DR

Store instants in UTC, display them in the reader local time, and never store a local time without recording which zone it belongs to. Check whether your timestamp is in seconds or milliseconds before you do anything else. Write dates as 2026-03-04, never 03/04/2026.

Seconds or milliseconds: check this first

The single most common timestamp bug is a factor of one thousand. Unix time is defined in seconds, but a great many systems hand you milliseconds, and the two are indistinguishable by eye unless you count digits.

The tell is length. A seconds timestamp for any date in the current era is 10 digits. A milliseconds timestamp is 13. Feed a 13-digit number to something expecting seconds and your date lands somewhere around the year 55000. Feed a 10-digit number to something expecting milliseconds and you get 20 January 1970, which is the classic "why is everything in 1970" symptom.

If a date looks absurd, count the digits before you debug anything else. A quick paste into the Unix timestamp converter will tell you in a second which unit you are actually holding.

A related trap: 0 is a perfectly valid timestamp. It means midnight UTC on 1 January 1970. When empty fields default to zero rather than to nothing, entire records quietly claim to have been created at the dawn of the epoch.

Store in UTC, display in local

The rule that holds up under pressure is short: store instants in UTC, convert to local time only at the moment of display, and never persist a local time without the zone it was written in.

The reason is that local time is lossy. "14:30" tells you nothing on its own. "14:30 in Berlin" is recoverable. A UTC instant is recoverable by anyone, anywhere, forever, and it also sorts correctly as plain text, which local times do not.

The one honest exception is a time that is genuinely about the clock rather than the instant. A shop opening at 09:00 opens at 09:00 all year, before and after the clocks change. That is a wall clock value plus a zone name, not an instant, and forcing it into UTC will make it wrong for half the year.

What you are storingThe right formatThe trap if you get it wrong
When something happenedUTC instant, or ISO 8601 with an offsetA local time with no zone is unrecoverable once the server moves
A future appointmentLocal time plus the zone name, for example Europe/LondonA fixed offset drifts by an hour when the rules change
A recurring opening timeWall clock time plus the zone nameStored as UTC it shifts by an hour after the clocks change
A birthday or an anniversaryA plain date with no time and no zoneMidnight UTC becomes the previous day west of Greenwich
A duration or an elapsed timeA count of seconds, not two datesSubtracting local times silently gains or loses an hour twice a year

An offset is not a time zone

This is the distinction almost everyone collapses, and it is the one that breaks scheduling. An offset is a number: +01:00 means one hour ahead of UTC. A time zone is a set of rules with a history and a future: Europe/London says the offset is +00:00 in winter and +01:00 in summer, and it knows the dates on which that flips.

So +01:00 is not London. Half the year London is +00:00. Storing an offset records what the clock happened to read, not the place, and the place is what you actually needed.

For anything in the past this rarely matters, because the offset was correct at the instant it was recorded. For anything in the future it matters enormously. A meeting saved as "2026-11-05 09:00+01:00" will be an hour out if the rules move, whereas "2026-11-05 09:00 in Europe/London" stays right by definition.

Governments change these rules with little notice, and they change them often. That is precisely why the shared time zone database exists and why "Europe/London" is a more durable thing to store than any number of hours. When you just need to know what a meeting time means for someone elsewhere, the time zone converter does the arithmetic without you having to reason about which half of the year you are in.

The hour that happens twice, and the hour that never was

Daylight saving produces two edge cases a year, and both are real bugs rather than curiosities.

In autumn, when the clocks go back, one local hour occurs twice. In the United Kingdom, 01:30 on that Sunday happens once at +01:00 and again an hour later at +00:00. A local time in that window is genuinely ambiguous, and something has to decide which of the two instants it means. Two events an hour apart can appear to have happened at the same time, or in the wrong order.

In spring, when the clocks go forward, one local hour never exists at all. Clocks jump from 00:59 to 02:00, so 01:30 on that date is not a real time. If someone asks for an alarm at that moment, the software must either skip it, fire it early, or fire it late. There is no correct answer, only a documented choice.

This is also why "a day" is not always 24 hours and why subtracting one local date from another quietly produces off by one errors. Do the arithmetic on instants, not on clock readings, and use the date difference calculator or the duration calculator rather than trusting a subtraction you did in your head.

The 2038 problem, and the leap second that is not there

A signed 32-bit integer counting seconds runs out at 03:14:07 UTC on 19 January 2038. One second later it overflows and wraps to a negative number, which reads as December 1901. This is the same shape of problem as the year 2000, and it is being fixed the same way: by widening the counter to 64 bits, which pushes the limit far beyond any horizon that matters.

Most modern systems already store 64-bit timestamps and are fine. The risk sits in old embedded devices, ancient file formats, and code that stuffed a timestamp into a 32-bit field years ago because it seemed like plenty. If you maintain anything with a long-dated calculation, such as a 30-year mortgage schedule, it is worth checking now rather than in 2037.

One deliberate quirk worth knowing: Unix time ignores leap seconds. It pretends every day is exactly 86400 seconds long, so the count is not a true tally of elapsed physical seconds since 1970. This is a feature, not an oversight. It keeps the arithmetic simple, and for everything short of scientific timekeeping the simplicity is worth far more than the accuracy it costs.

Write dates the way ISO 8601 does

When a date is written for a human, use the international standard order: year, month, day. 2026-03-04 is the fourth of March 2026 and it cannot be read any other way.

Compare that with 03/04/2026, which is the fourth of March to a British reader and the third of April to an American one. Both readings are perfectly reasonable, both are common, and nothing in the string tells you which was intended. This is not a pedantic distinction. It is a genuine, routine source of appointments booked a month out.

The standard order has a second benefit: it sorts correctly as plain text. Filenames, log lines, and spreadsheet columns all fall into chronological order for free, which no other common date format manages.

For a full instant, the same standard writes 2026-03-04T14:30:00Z, where the trailing Z means UTC. Add an explicit offset such as +01:00 when the local reading matters. It looks clinical, but it is the only widely used format that is impossible to misread.

The short version

Treat a timestamp as an instant, not a clock reading. Check whether it is in seconds or milliseconds before you trust any date it produces. Store instants in UTC, convert at display time, and never save a local time without its zone.

Remember that an offset records a moment while a zone name records a rule, and that only the rule survives into the future. Expect one duplicated hour and one missing hour every year, and handle both on purpose rather than by accident.

Do all of that and the whole subject becomes boring, which is exactly where you want it.

Tools from this guide

Keep reading