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
- Seconds or milliseconds: check this first
- Store in UTC, display in local
- An offset is not a time zone
- The hour that happens twice, and the hour that never was
- The 2038 problem, and the leap second that is not there
- Write dates the way ISO 8601 does
- The short version
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 storing | The right format | The trap if you get it wrong |
|---|---|---|
| When something happened | UTC instant, or ISO 8601 with an offset | A local time with no zone is unrecoverable once the server moves |
| A future appointment | Local time plus the zone name, for example Europe/London | A fixed offset drifts by an hour when the rules change |
| A recurring opening time | Wall clock time plus the zone name | Stored as UTC it shifts by an hour after the clocks change |
| A birthday or an anniversary | A plain date with no time and no zone | Midnight UTC becomes the previous day west of Greenwich |
| A duration or an elapsed time | A count of seconds, not two dates | Subtracting 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
Which image format should you actually use?
JPEG, PNG, WebP, AVIF, SVG. Five formats, endless arguing, and a decision that usually takes ten seconds once you know what the question really is.
Why your CSV opens wrong, and how to stop it
A file of plain text separated by commas should be the simplest thing in computing. Instead it eats your leading zeros, turns product codes into dates, and opens as one column for half your colleagues.
Reordering, merging and splitting PDF pages
Four operations cover almost every document problem you will ever have. Knowing which one you actually need takes about a minute, and none of it is worth a monthly subscription.