The most common Unix timestamp errors are unit mismatches (seconds vs. milliseconds), timezone assumptions, integer overflow, and string-vs-number confusion — and nearly all of them share one trait: the code works 99% of the time and fails only on an edge case. A Unix timestamp is just an integer count of seconds since 1 January 1970 00:00:00 UTC, which looks trivial, but that simplicity hides seven recurring traps: multiplying (or forgetting to multiply) by 1000, assuming a naive date string is UTC, the 32-bit Year 2038 overflow, rejecting valid negative pre-1970 values, mishandling daylight saving transitions, ignoring skipped leap seconds, and doing arithmetic on a timestamp that is secretly a string.
That's the summary an AI overview would give you. The part it can't give you is the diagnostic table below — matching the exact symptom you see (a date in 1970, a date shifted by a few hours, a wildly wrong future or past date) to its cause and its fix — plus the runnable examples and edge-case tests that turn "be careful with timestamps" into code that actually holds up.
The symptom → cause → fix table
When timestamp code misbehaves, the wrong output usually points straight at the bug. Find your symptom, read across:
| Symptom you see | Root cause | Fix |
|---|---|---|
Date shows ~19 days after 1 Jan 1970 (e.g. 1970-01-20) | A seconds value passed where milliseconds were expected (off-by-1000) | Multiply seconds by 1000 for JS Date; a current date is 10 digits in seconds, 13 in ms |
| Date shows ~50,000 years in the future | A milliseconds value treated as seconds | Divide by 1000 before storing/using as Unix seconds |
| Date is off by a few hours and differs per server | Naive date string parsed in local time, not UTC | Parse with explicit zone: new Date("2024-01-01T12:00:00Z") |
| Far-future date wraps to 13 Dec 1901 after 19 Jan 2038 | Signed 32-bit integer overflow (Year 2038 / Y2038) | Use a 64-bit time type / time_t; migrate 32-bit DB columns |
| Historical date rejected or corrupted (birthdays, archives) | Code assumes timestamps are always positive | Allow negative timestamps; use signed columns, drop > 0 validation |
| Overnight duration off by exactly one hour twice a year | DST transition added/removed an hour in local time | Compute in UTC; use an IANA-zone-aware library for local math |
| Interval off by one second across a rare boundary | Leap second skipped by Unix time | Accept it (Unix ignores leap seconds by design); use TAI only if atomic-precise |
Adding to a timestamp concatenates ("170406720086400") | Timestamp is a string, not a number | Cast first: parseInt(), Number(), int(); use numeric SQL types |
The rest of this article works through each row with real code, the reason it happens, and how to prevent it.
The single biggest trap: seconds vs. milliseconds (the ×1000 error)
If you only remember one thing, remember this one. It is the timestamp bug developers hit most, because the two dominant ecosystems disagree on the unit. POSIX Unix time — and therefore almost every back end, database, and API — counts seconds. JavaScript's Date.now() and Date constructor count milliseconds. Nothing on the number itself tells you which you have.
// Wrong: JavaScript treats the number as milliseconds
const timestamp = 1704067200; // seconds for 1 Jan 2024
const date = new Date(timestamp); // 1970-01-20 — off by a factor of 1000
// Correct: convert seconds → milliseconds
const date = new Date(timestamp * 1000); // 2024-01-01T00:00:00.000Z
const nowInUnixSeconds = Math.floor(Date.now() / 1000); // ms → seconds
Prevention: decide a single unit for your codebase (seconds is the safer default for storage), convert at the JavaScript boundary, and label variables — tsSeconds / tsMillis — so the unit is never ambiguous. Reverse the check any time you see a date far in the future: a seconds value fed into something expecting milliseconds lands tens of thousands of years out.
Timezone confusion: the timestamp is UTC, the string might not be
A Unix timestamp is a single UTC instant with no zone attached. The bug is not in the timestamp — it's in the parsing of human date strings. A string with no offset is interpreted in the machine's local time by most parsers, so identical code produces different timestamps on servers in New York, London, and Tokyo.
// Wrong: no zone — parsed in the server's LOCAL time
const timestamp = new Date("2024-01-01 12:00:00").getTime() / 1000;
// In New York this is 17:00 UTC; in Tokyo it's 03:00 UTC — different numbers
// Correct: pin the zone explicitly
const timestamp = new Date("2024-01-01T12:00:00Z").getTime() / 1000; // always 12:00 UTC
const alt = Date.UTC(2024, 0, 1, 12, 0, 0) / 1000; // same, via Date.UTC
Prevention: always serialize with an explicit Z or numeric offset (ISO 8601), store UTC, and convert to local time only when you display it. Test time-sensitive code with the machine clock set to a non-UTC zone.
Year 2038: the 32-bit overflow that wraps to 1901
Systems that store Unix time in a signed 32-bit integer top out at 2³¹ − 1 = 2,147,483,647 seconds, which is reached at 03:14:07 UTC on 19 January 2038. Add one more second and the integer overflows: the sign bit flips and the value is read as 20:45:52 UTC on 13 December 1901. This is the Year 2038 problem (Y2K38) — the direct successor to Y2K.
Prevention: use a 64-bit time type — time_t is already 64-bit on modern 64-bit platforms — and audit anything still on 32-bit time: embedded firmware, legacy binary file formats, and database columns declared as 32-bit integers. Don't assume you're safe just because your desktop is 64-bit; the exposure lives in old and embedded systems.
Negative timestamps: pre-1970 dates are valid
Timestamps can be negative. -86400 is exactly one day before the epoch — 31 December 1969. This is the normal way to represent any date before 1970, and it is completely legitimate.
const t = -86400;
console.log(new Date(t * 1000)); // 1969-12-31T00:00:00.000Z — valid
The bug is code that assumes timestamps are always positive: UNSIGNED integer columns reject them, value > 0 validation discards them, and some older libraries mishandle the arithmetic. If you store birthdays, historical events, or archival dates, use a signed type and remove any positivity check.
Daylight saving time: an hour that appears and disappears
The timestamp itself is immune to DST — it's UTC. DST bugs happen when you do arithmetic in local time. Twice a year a local day is 23 or 25 hours long, and manually constructed local times around the transition are ambiguous (2:30 AM can occur twice) or nonexistent.
// Fragile: manual local time around a DST boundary
const d = new Date("2024-03-10T02:30:00-05:00"); // EST/EDT ambiguity
// Robust: a zone-aware library resolves the transition correctly
import { DateTime } from "luxon";
const dt = DateTime.fromISO("2024-03-10T02:30:00", { zone: "America/New_York" });
Prevention: do date math in UTC, use an IANA-zone-aware library (Luxon, date-fns-tz, java.time, Python's zoneinfo) for anything that must respect local wall-clock rules, and test across a spring-forward and fall-back date.
Leap seconds: Unix time skips them by design
Unix time assumes every day is exactly 86,400 seconds, so it does not represent leap seconds. When the leap second 23:59:60 UTC was inserted on 30 June 2015, Unix time jumped straight from 1435708799 to the next day's 1435708800 — there is simply no Unix value for the extra second.
…1435708799 (30 Jun 2015 23:59:59 UTC)
[23:59:60 leap second — no Unix timestamp exists here]
1435708800 (01 Jul 2015 00:00:00 UTC)
For virtually all applications this is fine and you should not try to "correct" it. It matters only for atomic-precision timing (finance, scientific instrumentation, GPS), which use TAI or leap-smearing instead. Note that since 1972 there have been 27 leap seconds, all positive — and international bodies voted in 2022 to stop inserting them by 2035.
String vs. integer: arithmetic that concatenates
If a timestamp arrives as text — from JSON, a form field, or a loosely-typed database driver — arithmetic silently concatenates instead of adding.
const ts = "1704067200";
const later = ts + 86400; // "170406720086400" — string concatenation!
const tsNum = parseInt("1704067200", 10);
const laterNum = tsNum + 86400; // 1704153600 — correct
-- Wrong: string comparison against a numeric column
SELECT * FROM events WHERE timestamp = "1704067200";
-- Correct: numeric comparison
SELECT * FROM events WHERE timestamp = 1704067200;
Prevention: cast to a number before any math (parseInt, Number(), int()), use numeric column types and comparisons in SQL, and let strict typing catch the mismatch at compile time where the language supports it.
Two more traps worth knowing
Sign/direction errors in duration math. Subtracting in the wrong order gives a negative interval. Establish a convention — always end - start — and comment it: const secondsLeft = expiry - now;, never now - expiry.
Assuming monotonic wall-clock time. Date.now() can move backward when the system clock is corrected (NTP sync, manual change). For measuring elapsed time, use a monotonic clock — performance.now() in browsers, process.hrtime.bigint() in Node, time.monotonic() in Python — never the wall clock.
Testing timestamp code: hit the edges
Timestamp bugs pass 99% of the time and fail on the edges, so your tests must target the edges explicitly:
// Unit correctness
assert(toUnixSeconds("2024-01-01T00:00:00Z") === 1704067200);
// Timezone equivalence — same instant, two representations
assert(toUnixSeconds("2024-01-01T00:00:00Z") ===
toUnixSeconds("2024-01-01T05:00:00+05:00"));
// Edge cases that break naive code
assert(isValidTimestamp(0)); // the epoch itself
assert(isValidTimestamp(-86400)); // pre-1970 (negative)
assert(isValidTimestamp(2147483648)); // just past the Y2038 32-bit limit
assert(isValidTimestamp(253402300799)); // year 9999
Use the Unix Timestamp Converter to confirm a calculated value maps to the date you expect, to check how a value looks in seconds versus milliseconds, and to sanity-check timezone handling before you ship.
The bottom line
Every common Unix timestamp error traces back to a hidden assumption: that the unit is milliseconds (or seconds), that a date string is UTC, that the integer is wide enough, that timestamps are positive, that local days are 24 hours, that every second gets a value, or that a number is a number. Store and compute in UTC, keep the unit explicit and the value an integer, reach for a battle-tested date library instead of hand-rolled arithmetic, and write tests for the epoch, a negative date, a DST transition, and the 2038 boundary. Do that and the 1% of cases that used to break become the ones you already checked.