Date and time handling has a reputation for being fiddly, and it earns it — but the trouble clusters around a few specific misunderstandings rather than being uniformly hard. Get those right and the rest falls into place. You can convert between epoch values and human dates on the timestamp converter while reading.
What a Unix timestamp is
A Unix timestamp is the number of seconds since midnight UTC on 1 January 1970, a moment called the epoch. Its great virtue is that it's a single number in UTC with no time zone attached, so the same instant has the same value everywhere on Earth — unambiguous and trivial to compare or sort. That's exactly why logs, databases, APIs and JWTs store time as an epoch number and convert to something human-readable only when they need to show it.
Seconds vs milliseconds is the most common bug. Unix time is in seconds, but JavaScript's Date.now() returns milliseconds. Mix them and you're off by a factor of 1000 — a value lands in 1970 or tens of thousands of years from now. The quick tell: a 10-digit number is seconds, a 13-digit number is milliseconds.
// JavaScript: Date.now() is milliseconds, so divide for seconds
const unixSeconds = Math.floor(Date.now() / 1000);
# Python: time.time() already returns seconds
import time
unix_seconds = int(time.time())
Store UTC, display local
Nearly every nasty time-zone bug comes back to one thing: a local time saved without recording which zone it belonged to. The discipline that prevents it fits in a sentence — keep everything in UTC internally, and translate to a local zone only at the outer edge, the moment a human actually looks at it. Because a Unix timestamp carries no zone and is UTC by construction, it's the natural value to hand a database or another service. Write down "3:00 PM" with no zone attached and you've thrown away something you can't recover, since that one reading points at a different instant depending on whether you're standing in Tokyo or New York.
And offsets aren't constant the way people picture them. They move when daylight saving kicks in, and the rules behind that are political — governments revise them, sometimes with little warning. So mature systems never hard-code an offset like "UTC−5"; they name a region such as America/New_York and let the IANA time-zone database resolve the correct offset for any given date, history of past changes included.
Clocks that run twice, and days that aren't 24 hours
Daylight saving does genuinely strange things that break naive code. On the night clocks spring forward, a whole hour simply doesn't exist — in much of the US, 2:30 AM never happens that day. On the night they fall back, 1:30 AM happens twice, so "1:30 AM local" is ambiguous. This means a day can be 23 or 25 hours long, "tomorrow at the same time" isn't always 24 hours away, and scheduling "every day at 2:30 AM" can fire zero times or twice on the wrong nights. None of this is your bug to fix — it's the world being messy — and storing instants in UTC rather than local wall-clock times sidesteps the entire category. When you must reason about local time (a daily report, a reminder), do it through a real time-zone library, never by adding or subtracting fixed hour offsets.
ISO 8601, the format that sorts
When a date has to be written as text — in an API response, a log, a filename — ISO 8601 (and its stricter web cousin RFC 3339) is the safe choice: 2026-06-19T08:30:00Z. It's unambiguous, the trailing Z means UTC, and because the fields go from largest to smallest, the string sorts chronologically as plain text. That last property is quietly valuable: sort ISO timestamps alphabetically and they're also in time order, which makes log files and filenames behave. Avoid locale formats like 06/07/2026 in anything machine-read — that's June 7th in the US and July 6th almost everywhere else, and the ambiguity has caused real outages.
Don't use the wall clock as a stopwatch
Timing an operation by subtracting two Date.now() readings looks obvious and is a trap. A wall clock is allowed to leap around — an NTP sync corrects it, someone edits the system time, a daylight-saving change moves it — so the gap between two readings can come out negative or absurd. To measure elapsed time, reach for a monotonic clock, guaranteed never to run backwards: performance.now() in browsers, time.monotonic() in Python. The split is real — one clock reports the time of day, the other reports how much has passed, and stopwatch code wants the second one.
Database types and the Year 2038
In the database, prefer a type that records the instant unambiguously. In Postgres that's timestamptz (despite the name, it stores a UTC instant rather than a zone) over a plain timestamp that quietly drops zone information. And keep one eye on the Year 2038 problem: systems that store Unix time in a signed 32-bit integer overflow on 19 January 2038, wrapping into negative numbers. Modern languages and 64-bit timestamps push that limit hundreds of billions of years out, but legacy systems, old embedded devices and the occasional database column still need checking before it becomes this generation's Y2K.
The whole topic comes down to a few habits: store UTC, display local through a real time-zone database, write dates in ISO 8601, never reach for fixed offsets, and use a monotonic clock when you're timing rather than telling.