A date is not an instant until you say where
A Unix timestamp identifies a moment in the history of the universe. A calendar date and a time of day identify a reading on a clock — and clocks disagree. Converting from the second to the first therefore needs one extra piece of information that the date itself does not carry: which clock. Every tool that converts dates to timestamps supplies that piece somehow. The dangerous ones supply it silently, from the server's environment, and hand you a number that changes when the deployment moves.
The converter above asks instead. Pick UTC and the reading is treated as a clock in
Greenwich; pick your zone and it is treated as the clock on your wall, with whatever offset was in
force on that particular date. The output panel restates which one it used and prints the offset it
applied, so a screenshot of the result is self-explanatory.
One wall clock, eleven instants
Here is the cost of leaving the zone implicit, in numbers. Every row below is the same reading — 2024-06-15 12:00 — on a clock in a different place. The epoch column shows what each one converts to. They are all correct; they are all different.
| Zone | Offset | Epoch seconds | Same instant in UTC | vs UTC row |
|---|---|---|---|---|
Pacific/Chatham Chatham Islands, NZ | +12:45 | 1718406900 | 2024-06-14T23:15:00.000Z | −12.75 h |
Australia/Adelaide Adelaide, Australia | +09:30 | 1718418600 | 2024-06-15T02:30:00.000Z | −9.5 h |
Asia/Tokyo Tokyo, Japan | +09:00 | 1718420400 | 2024-06-15T03:00:00.000Z | −9 h |
Asia/Kathmandu Kathmandu, Nepal | +05:45 | 1718432100 | 2024-06-15T06:15:00.000Z | −5.75 h |
Asia/Kolkata Kolkata, India | +05:30 | 1718433000 | 2024-06-15T06:30:00.000Z | −5.5 h |
Europe/Berlin Berlin, Germany | +02:00 | 1718445600 | 2024-06-15T10:00:00.000Z | −2 h |
Europe/London London, UK | +01:00 | 1718449200 | 2024-06-15T11:00:00.000Z | −1 h |
UTC UTC / Etc | +00:00 | 1718452800 | 2024-06-15T12:00:00.000Z | — |
America/St_Johns St John's, Canada | -02:30 | 1718461800 | 2024-06-15T14:30:00.000Z | +2.5 h |
America/New_York New York, USA | -04:00 | 1718467200 | 2024-06-15T16:00:00.000Z | +4 h |
America/Los_Angeles Los Angeles, USA | -07:00 | 1718478000 | 2024-06-15T19:00:00.000Z | +7 h |
The spread from the top row to the bottom is 71,100 seconds, which is 19.75 hours — over four fifths of a day between the earliest and latest instant that can honestly be called "2024-06-15 12:00", and these eleven zones are only a sample. Across the whole tz database the extreme is wider still: UTC+14 to UTC−12 is 26 hours, so the same wall clock can span more than a calendar day end to end. Tokyo reaches that reading 9 hours before UTC does; Los Angeles gets there 7 hours after. And 5 of the 11 rows are not whole-hour offsets at all — +12:45, +09:30, +05:45, +05:30, -02:30 — which is why code that models a zone as an integer number of hours is broken for real users, not just theoretically.
Worked example: the report that lost a day
A daily revenue job filters with created_at >= '2024-06-15 00:00:00' and the
European and American teams get different totals for the same day. Nothing is wrong with the data.
The query string has no zone, so the database applies its session zone, and the two application
servers have different ones. Run the boundary through this page under each zone and the two
timestamps are hours apart — every order that fell in that gap is counted once by one team and not
at all by the other.
The fix is not a better query string; it is converting the boundary to an explicit instant before it reaches the database. Pick the date here with the zone set deliberately, take the epoch seconds, and filter on that. The number is the same everywhere, which is the only property that makes two reports comparable. If you need a per-region day, convert two boundaries per region rather than hoping one string means the same thing twice.
Rounding, and the values you get back
The picker works to whole seconds, so the millisecond output always ends in three zeros and the microsecond output in six. That is deliberate: an interface offering millisecond precision on a field you type by hand invites a false sense of resolution. If you need sub-second precision, encode the whole second here and add the remainder in code — or paste the full precision value into the decode direction, which keeps fractional seconds intact.
Dates that do not exist are rejected rather than adjusted. Type 31 April and you get an error, not 1 May. This matters more than it sounds: silent rollover is how a validation bug becomes a wrong date in a database, and it is the default behaviour of several popular date constructors.
Related conversions
- Unix timestamp to date — the decode direction, with the digit-length table behind unit auto-detection.
- Current Unix timestamp — the live count, and the milestone table from the epoch to the 2038 ceiling.
- Milliseconds to date — what to do when the value you are handed has 13 digits.
- Discord timestamp generator — encode an instant once and let every reader see it in their own zone automatically.
If you are scheduling rather than recording — "run this at 03:00 every weekday" — a timestamp is the wrong tool entirely, and the pitfalls are the same ones in a different costume. Our cron expression builder covers that side, including how the daemon's own zone decides what "03:00" means.