Dates are the subject where confident developers ship confident bugs. Nearly all of them come from one mistake made in four different places: treating a moment in time and a reading on a clock as the same thing.
What a Unix timestamp actually is
A count of seconds since 1 January 1970, 00:00:00 UTC. That is all. It has no timezone, because it is not a clock reading — it is a distance from a fixed point, the same number everywhere on Earth at the same instant.
1755475200 is a moment. What clock face that moment corresponds to depends entirely on where you are standing, and the timestamp neither knows nor cares.
Two things routinely trip people up:
- Seconds or milliseconds. Unix time is seconds; JavaScript’s
Date.now()is milliseconds. Mixing them puts you in 1970 or in the year 57000, which is at least an obvious failure. The converter takes either and says which it read. - Leap seconds are ignored. Unix time pretends every day has exactly 86,400 seconds. It is a count of an idealised day, not of actual elapsed seconds, which is fine for everything except precision timing.
UTC is not a timezone
UTC is the reference all timezones are offsets from. It never changes and has no daylight saving. GMT is, for everyday purposes, the same thing — with the awkward difference that the UK uses GMT in winter and BST in summer, so “GMT” in a British context sometimes means “local time” and sometimes means UTC.
Which is why +00:00 is a better thing to write than “GMT” in anything a machine will read.
The rule that prevents most date bugs
Store the moment. Convert on display.
Keep timestamps in UTC — as a Unix number or an ISO string with an offset — everywhere in your database, your logs and your APIs. Convert to the viewer’s local time only when you render it, and never store the converted value.
The moment a local time is stored without its offset, the information needed to interpret it is gone for good. “14:00” is not a time; it is a time in some timezone nobody wrote down.
ISO 8601, and the one detail that matters
2026-08-18T14:30:00Z — year, month, day, then time, with a trailing Z meaning UTC. Or an explicit offset: 2026-08-18T14:30:00+05:00.
The detail: a string with no offset — 2026-08-18T14:30:00 — is ambiguous, and different languages resolve it differently. Some assume UTC, some assume local. Always include the offset.
It also sorts correctly as plain text, which is why it is the right format for filenames and log lines.
Four bugs everyone ships once
The off-by-an-hour that appears in spring
Code that adds 86,400 seconds to get “tomorrow” is right 363 days a year. On the two days a region changes its clocks, a day is 23 or 25 hours long, and the result lands an hour out. Add one day using a date library, not 24 hours using arithmetic.
Times that do not exist, and times that happen twice
When clocks go forward, an hour is skipped — in the UK, 01:30 on that morning simply never occurs. When they go back, 01:30 happens twice. Scheduling anything in that window is genuinely ambiguous, and a recurring job set for 01:30 will either be skipped or run twice a year.
This is the commonest cause of a cron job firing at an unexpected time. The cron builder shows the next runs in your own timezone precisely so the mismatch with a UTC server is visible.
Offsets are not fixed
A timezone is not an offset — it is a set of rules about which offset applies when, and governments change those rules with little notice. Storing “+05:30” records what the offset was; storing Asia/Kolkata records the rule, which is what you want for anything in the future. This is why recurring calendar events need a named zone rather than a number.
The date changes before the day does
A user in Auckland and one in Los Angeles are on different calendar dates for most of the day. “Today’s orders” means two different sets of rows depending on whose midnight you use — and a daily report that runs in UTC will look wrong to everyone not in UTC. Decide whose day you mean and write it down.
When a local time is the right thing to store
Occasionally the clock reading genuinely is the fact. A shop opens at 09:00 local time whatever the offset that week; a birthday is a date, not a moment; an alarm for 07:00 should ring at 07:00 wherever you have flown to.
For those, store the local value and the rule — never a converted UTC instant, which will drift the moment the rules change or the user moves. Recognising which kind you have is most of the skill.
2038
A 32-bit signed timestamp runs out on 19 January 2038 and wraps to 1901. Modern systems use 64-bit values and are fine for longer than the sun will last, but 32-bit fields survive in embedded devices, old file formats and database columns nobody has looked at. Worth checking if you maintain anything of that age.
Converting
Timestamp converter for reading an epoch value both ways, timezone converter for seeing one moment across several zones, and date difference for counting days, weeks and working days between two dates.