Developer Tools

What Are Common Errors When Working With Unix Timestamps?

The seven bugs that catch every developer working with Unix timestamps — seconds-vs-milliseconds, timezone assumptions, Y2038 overflow, negative dates, DST, leap seconds, and string-vs-int — with a symptom to cause to fix lookup table.

By Inventive HQ Team

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 seeRoot causeFix
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 futureA milliseconds value treated as secondsDivide by 1000 before storing/using as Unix seconds
Date is off by a few hours and differs per serverNaive date string parsed in local time, not UTCParse with explicit zone: new Date("2024-01-01T12:00:00Z")
Far-future date wraps to 13 Dec 1901 after 19 Jan 2038Signed 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 positiveAllow negative timestamps; use signed columns, drop > 0 validation
Overnight duration off by exactly one hour twice a yearDST transition added/removed an hour in local timeCompute in UTC; use an IANA-zone-aware library for local math
Interval off by one second across a rare boundaryLeap second skipped by Unix timeAccept 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 numberCast 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.

The times-1000 trap: one number read two ways The value 1704067200 read as seconds resolves to 1 January 2024, but read as milliseconds it resolves to 20 January 1970 — the classic off-by-1000 bug. One number, two very different dates 1704067200 the same 10-digit integer… read as SECONDS ✓ 1 Jan 2024 00:00:00 UTC ts * 1000 read as MILLISECONDS ✗ 20 Jan 1970 ~19.7 days after epoch new Date(ts) A date near 1970 is the fingerprint of the off-by-1000 bug.
// 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.

Loading interactive tool...

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.

Advertisement

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.

The Year 2038 signed 32-bit overflow At 2,147,483,647 seconds the 32-bit counter overflows from 19 January 2038 and wraps back to 13 December 1901. 2,147,483,647 → overflow → back to 1901 1970 0 19 Jan 2038 2147483647 03:14:07 UTC sign bit flips → interpreted as 13 Dec 1901 Fix: 64-bit time_t pushes the limit ~292 billion years out.

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.

Frequently Asked Questions

Why does my Unix timestamp show a date in 1970?

Almost always because a value in seconds was fed into something that expects milliseconds. JavaScript's Date object counts milliseconds since the epoch, so passing a normal 10-digit seconds value like 1704067200 puts you about 19.7 days after 1 January 1970 — the year-1970 date is the classic symptom of the off-by-1000 bug. The fix is to multiply seconds by 1000 before constructing a JavaScript Date, or divide milliseconds by 1000 before storing a Unix seconds value. A quick sanity check: a seconds timestamp for a current date is 10 digits, a milliseconds one is 13.

Are Unix timestamps in seconds or milliseconds?

The original POSIX definition of Unix time is seconds since 1 January 1970 00:00:00 UTC, and most back-end systems, databases, and APIs use seconds. JavaScript is the big exception: Date.now() and the Date constructor use milliseconds. There is no flag on the number itself to tell them apart, so you have to know which unit a given source produces. When in doubt, look at the magnitude — a 10-digit number is seconds, a 13-digit number is milliseconds for any date near the present.

Do Unix timestamps include timezone information?

No. A Unix timestamp is a single instant measured in UTC and carries no timezone at all — the same number means the same moment everywhere on Earth. Timezones only enter when you format that instant into a human-readable date. The common bug is parsing a date string with no zone (like "2024-01-01 12:00:00"), which most libraries interpret in the machine's local time, so the same code produces different timestamps on servers in different regions. Always attach an explicit offset or a Z suffix (2024-01-01T12:00:00Z) so the parse is deterministic.

What is the Year 2038 problem?

Systems that store Unix time in a signed 32-bit integer can only count up to 2,147,483,647 seconds, which is reached at 03:14:07 UTC on 19 January 2038. One second later the integer overflows, the sign bit flips, and the value is misread as 20:45:52 UTC on 13 December 1901. The fix is to use a 64-bit time type (time_t is already 64-bit on modern 64-bit systems), which pushes the limit roughly 292 billion years into the future. The risk today lives mostly in embedded devices, old file formats, and databases that still use 32-bit time columns.

Can a Unix timestamp be negative?

Yes. Negative timestamps represent moments before the epoch — for example, -86400 is 31 December 1969. This is completely valid and is how dates before 1970 are stored. Bugs appear when code assumes timestamps are always positive: unsigned integer columns reject them, validation that requires a value greater than zero throws them out, and some older libraries mishandle the arithmetic. If you deal with historical dates (birthdays, archives, ancient events), make sure your storage type and validation allow negative values.

Do Unix timestamps account for leap seconds?

No. Unix time pretends every day has exactly 86,400 seconds, so it silently skips the inserted leap second rather than counting it. When the leap second at 23:59:60 UTC on 30 June 2015 was added, Unix time went straight from 1435708799 to the next day's 1435708800 with no value for the extra second. This keeps the arithmetic simple but means a Unix interval is not a true count of elapsed SI seconds across a leap-second boundary. It only matters for atomic-precision timing; note that the international bodies have voted to stop adding leap seconds by 2035.

How do I stop treating a timestamp as a string?

Parse it to a number before doing arithmetic. If a timestamp arrives from JSON, a form, or a database driver as text, adding to it concatenates instead of adding — "1704067200" + 86400 becomes "170406720086400". Convert explicitly with parseInt, Number(), int(), or your language's integer cast, and use numeric (not string) column types and comparisons in SQL. Strict typing catches this at compile time in typed languages.

What is the safest general strategy for handling timestamps?

Store and compute in UTC, use a well-tested date library instead of hand-rolled arithmetic, and only convert to local time at the moment of display. Keep timestamps as integers, be explicit about whether you are working in seconds or milliseconds, and write test cases for the edge dates — the epoch itself, a negative pre-1970 value, a DST transition, and a far-future date. Most timestamp bugs are edge-case bugs that pass 99% of the time, so testing the edges is where the value is.

unix-timestampdebuggingcommon-mistakesdate-timedevelopment