← All posts

10 Common Unix Timestamp Bugs and How to Fix Them

Most timestamp bugs come from three mistakes: the unit is unclear, the timezone is implicit, or wall-clock time is used for elapsed-time logic. This guide shows the symptom, root cause, fix, regression test, and code-search recipe for ten production-grade Unix timestamp bugs.

Quick triage

When a timestamp bug appears in production, start with three questions:

Question Fast check Common symptom
What unit is it? 10 digits seconds, 13 milliseconds, 16 microseconds, 19 nanoseconds date lands in 1970 or year 55000
What timezone is used for display? UTC, user's IANA timezone, or machine default? laptop and server disagree
Is this an instant or a local calendar rule? log event vs "every Monday at 9 AM" DST and recurring schedules drift

Use a known timestamp while debugging:

1700000000 seconds      = 2023-11-14T22:13:20Z
1700000000000 ms        = 2023-11-14T22:13:20Z
1700000000000000 micros = 2023-11-14T22:13:20Z

If your result is not near November 2023, you have a unit bug before you have a timezone bug.

Bug 1: Passing Unix seconds to JavaScript Date

Symptom: a recent timestamp displays as January 1970.

new Date(1700000000).toISOString();
// "1970-01-20T16:13:20.000Z"

Root cause: JavaScript Date expects milliseconds since the Unix epoch. Many APIs, JWT claims, shell tools, databases, and webhook payloads use Unix seconds.

Fix:

const createdAtSeconds = 1700000000;

new Date(createdAtSeconds * 1000).toISOString();
// "2023-11-14T22:13:20.000Z"

Safer boundary function:

function unixSecondsToDate(seconds) {
  if (!Number.isFinite(seconds)) {
    throw new TypeError("timestamp must be a number");
  }
  return new Date(seconds * 1000);
}

Avoid a generic timestamp field name. Prefer:

created_at_seconds
createdAtMs
expires_at_seconds
event_time_us

Code search:

rg 'new Date\(\s*\d{10}\b'
rg 'new Date\([^)]*(created_at|createdAt|timestamp|expires)'

Regression test:

console.assert(
  new Date(1700000000 * 1000).toISOString() === "2023-11-14T22:13:20.000Z",
);

Bug 2: Mixing seconds, milliseconds, microseconds, and nanoseconds

Symptom: one service writes a valid number, another service reads a date thousands of years away.

new Date(1700000000000 * 1000).getUTCFullYear();
// 55840

That happened because epoch milliseconds were multiplied as if they were seconds.

Root cause: the API contract says "timestamp" but not the unit.

Common units:

Unit Example Usually from
seconds 1700000000 Unix tools, JWT NumericDate, many APIs
milliseconds 1700000000000 JavaScript Date, MongoDB Date, browser events
microseconds 1700000000000000 databases, logs, tracing systems
nanoseconds 1700000000000000000 Go, OpenTelemetry-style pipelines, high-resolution telemetry

Fix: convert once at the boundary and use unit-specific names inside your app.

type ApiUser = {
  created_at_seconds: number;
};

type ViewUser = {
  createdAtMs: number;
};

function mapUser(user: ApiUser): ViewUser {
  return {
    createdAtMs: user.created_at_seconds * 1000,
  };
}

Code search:

rg '\btimestamp\b|\bcreatedAt\b|\bupdatedAt\b|\bexpiresAt\b'
rg '\* 1000|/ 1000|1000000|1000000000'

Contract test:

function assertModernEpochMs(value) {
  if (value < 946684800000 || value > 4102444800000) {
    throw new RangeError("expected epoch milliseconds between 2000 and 2100");
  }
}

RFC 7519 uses NumericDate for JWT time claims, which is seconds from 1970-01-01T00:00:00Z, not milliseconds.

Bug 3: Letting server local timezone leak into output

Symptom: the same timestamp displays differently on a developer laptop, CI, and production.

new Date("2023-11-14T22:13:20Z").toLocaleString();
// depends on the runtime's default timezone

Root cause: toLocaleString() uses the runtime's local timezone unless you pass an explicit timeZone option.

Fix for server output:

new Intl.DateTimeFormat("en-US", {
  timeZone: "UTC",
  dateStyle: "medium",
  timeStyle: "long",
}).format(new Date("2023-11-14T22:13:20Z"));

Fix for user-facing output:

new Intl.DateTimeFormat("en-US", {
  timeZone: "America/New_York",
  dateStyle: "medium",
  timeStyle: "short",
}).format(new Date("2023-11-14T22:13:20Z"));
// "Nov 14, 2023, 5:13 PM"

Set servers to UTC for operational sanity, but still pass timeZone explicitly in code.

Code search:

rg 'toLocale(String|DateString|TimeString)\('
rg 'Date\(\)\.toString|new Date\(\)\.toString'

Review each match for an explicit timeZone.

Bug 4: Storing timestamps as free-form strings

Symptom: sorting and filtering look random.

-- Bad source-of-truth column
created_at = 'Jun 20, 2026 9:25am'

A database cannot reliably compare that as time. It compares text unless you parse it, and parsing can depend on locale, format, and database settings.

Fix: use a native timestamp type or a documented epoch integer.

Good SQL patterns:

-- PostgreSQL
created_at timestamptz NOT NULL DEFAULT now()
-- MySQL, app writes UTC
created_at_utc DATETIME(3) NOT NULL
-- Numeric event pipeline
created_at_ms BIGINT NOT NULL

Strict ISO 8601 strings can be acceptable in logs and some document stores:

2026-06-20T14:30:00Z

but do not use human-formatted strings as the source of truth.

Code/data search:

SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE column_name LIKE '%_at'
  AND data_type IN ('character varying', 'varchar', 'text');

Bug 5: Parsing ambiguous date strings

Symptom: 01/02/2026 means January 2 on one machine and February 1 somewhere else.

Date.parse("01/02/2026");
// implementation-defined for non-standard formats

Root cause: JavaScript only guarantees specific date-time string behavior. MDN notes that formats outside the standard invariants are implementation-defined.

Two subtle ISO rules matter:

new Date("2019-01-01").toISOString();
// "2019-01-01T00:00:00.000Z"  date-only is UTC
new Date("2019-01-01T00:00:00");
// local time, because no timezone offset is present

Fix: accept only strings with Z or an explicit offset at system boundaries.

new Date("2026-01-02T00:00:00Z");
new Date("2026-01-02T00:00:00-05:00");

If users type local dates, parse them as local dates with an explicit format and timezone. Do not send them through Date.parse() and hope.

Code search:

rg 'Date\.parse\('
rg "new Date\\([\\\"'].*[/-].*[\\\"']\\)"

Bug 6: Adding 24 hours to mean tomorrow

Symptom: "tomorrow at 9 AM" becomes 10 AM or 8 AM around daylight saving transitions.

const tomorrow = new Date(today.getTime() + 86_400_000);

That is elapsed-time arithmetic, not local calendar arithmetic.

Root cause: UTC days are modeled as 86,400,000 milliseconds, but local calendar days can be 23 or 25 hours when daylight saving time changes.

When fixed milliseconds are OK:

  • cache TTLs
  • elapsed-time windows
  • UTC-only log windows
  • "retry after 24 hours" as a duration

When fixed milliseconds are risky:

  • tomorrow in the user's timezone
  • calendar reminders
  • business-day cutoffs
  • recurring meetings
  • billing cycles based on local dates

Fix: use calendar arithmetic in the target timezone. If you use Temporal or a timezone-aware library, add one calendar day to the zoned date-time, not 86,400,000 milliseconds to the instant.

Code search:

rg '86400000|86_400_000|24\s*\*\s*60\s*\*\s*60\s*\*\s*1000'

Review every match that touches local time.

Bug 7: Inclusive end-of-day filters

Symptom: reports miss events in the last second, millisecond, or microsecond of the day.

WHERE created_at BETWEEN '2026-06-20 00:00:00'
                    AND '2026-06-20 23:59:59'

That drops:

2026-06-20 23:59:59.001
2026-06-20 23:59:59.999999

Fix: use a half-open range.

WHERE created_at >= '2026-06-20 00:00:00'
  AND created_at <  '2026-06-21 00:00:00'

For Unix seconds:

WHERE created_at_seconds >= 1781913600
  AND created_at_seconds <  1782000000

Code search:

rg 'BETWEEN.*23:59:59|23:59:59.*BETWEEN'
rg '<= .*endOfDay|end_of_day'

Regression test: insert a row at 23:59:59.999 and make sure the daily report includes it.

Bug 8: Assuming cron handles DST the way your app does

Symptom: a daily job is skipped, delayed, or handled specially during daylight saving changes.

Cron behavior depends on the cron implementation and distribution. Some implementations run skipped fixed-time jobs immediately after a spring-forward jump and avoid double-running jobs after a backward jump; others behave differently. The safe lesson is not "cron always skips" or "cron always doubles." The safe lesson is that local wall-clock schedules have DST edge cases.

Risky schedule:

30 2 * * * /app/bill-customers

In America/New_York, the local 02:30 time does not exist on spring-forward day.

Safer options:

CRON_TZ=UTC
30 7 * * * /app/bill-customers

or schedule outside the local transition window if the business truly requires local time.

Code search:

rg 'CRON_TZ|TZ=' /etc/crontab /etc/cron* 2>/dev/null
rg '^[0-9*,/-]+[[:space:]]+[123][0-9,*/-]*[[:space:]]+\*' /etc/crontab 2>/dev/null

Operational test: check the next run time before the DST weekends in every timezone where you run local cron.

Bug 9: Comparing Date objects by identity

Symptom: two identical-looking dates are not equal.

new Date(0) === new Date(0);
// false

Root cause: === compares object identity. These are two different objects.

Fix: compare the epoch millisecond value.

const a = new Date(0);
const b = new Date("1970-01-01T00:00:00Z");

a.getTime() === b.getTime();
// true

You can also coerce with unary plus:

+a === +b;
// true

Set/Map fix: use the numeric timestamp as the key.

const seen = new Set();
seen.add(date.getTime());

Code search:

rg 'Date.*===|===.*Date|!==.*Date|Date.*!=='

Bug 10: Using Date.now for elapsed-time measurement

Symptom: a benchmark or timeout duration is noisy, jumps, or sometimes becomes negative.

const started = Date.now();
doWork();
const elapsed = Date.now() - started;

Root cause: Date.now() is wall-clock time. Wall clocks can be adjusted by NTP, virtualization, manual changes, and OS time correction. It answers "what time is it?" not "how long did this take?"

Fix in browsers:

const started = performance.now();
doWork();
const elapsedMs = performance.now() - started;

Fix in Node.js:

const { performance } = require("node:perf_hooks");

const started = performance.now();
doWork();
const elapsedMs = performance.now() - started;

Use:

  • Date.now() for event timestamps and wall-clock records
  • performance.now() for durations, benchmarks, timeouts, and animation timing

Code search:

rg 'Date\.now\(\)' --glob '*{bench,perf,timing,test}*'
rg 'Date\.now\(\).*-'

Codebase sweep checklist

Run these searches before a release or after a timestamp incident:

Bug class Search
seconds passed to JS Date rg 'new Date\\(\\s*\\d{10}\\b'
ambiguous timestamp fields `rg '\btimestamp\b
missing timezone formatting `rg 'toLocale(String
string timestamp storage inspect *_at columns with text/varchar types
ambiguous parsing `rg "Date\.parse\(
24-hour local math `rg '86400000
inclusive end-of-day SQL `rg 'BETWEEN.*23:59:59
cron DST risk inspect local-time jobs between 01:00 and 03:30
Date object equality `rg 'Date.*===
wall-clock benchmarks rg 'Date\\.now\\(\\)' --glob '*{bench,perf,timing}*'

Do not auto-fix every match. Review each one for meaning. A timestamp used for display, storage, scheduling, and duration measurement has different rules.

Production prevention checklist

Use these defaults on new code:

  • Store exact events as UTC instants.
  • Store local schedules as local date, local time, and IANA timezone.
  • Put units in numeric field names: _seconds, _ms, _us, _ns.
  • Use native datetime/timestamp columns or documented epoch integers, not free-form strings.
  • Use strict ISO 8601/RFC 3339 strings at API boundaries when readability matters.
  • Pass timeZone explicitly in server-side formatting.
  • Use half-open date ranges: start <= value < end.
  • Use calendar arithmetic for local dates.
  • Run critical cron jobs in UTC, or explicitly test DST transition dates.
  • Compare JavaScript dates with .getTime().
  • Use performance.now() for elapsed time.

Official references

Related timestamp guides

FAQ

What is the most common Unix timestamp bug?
The most common bug is passing Unix seconds to an API that expects milliseconds, especially JavaScript Date. new Date(1700000000) lands in January 1970 because JavaScript treats the value as milliseconds. Use new Date(1700000000 * 1000) or name the field created_at_seconds.
How do I prevent seconds vs milliseconds bugs?
Put the unit in every numeric field name, such as created_at_ms, expires_at_seconds, event_time_us, or logged_at_ns. Convert once at the system boundary and add tests for 10-digit seconds, 13-digit milliseconds, and 16-digit microseconds.
Why does my timestamp show 1970?
A modern Unix seconds value was probably treated as milliseconds. 1700000000 seconds is 2023-11-14T22:13:20Z, but 1700000000 milliseconds is 1970-01-20T16:13:20Z.
Why does Date.parse give different results?
JavaScript only guarantees a small set of date string formats. Date-only ISO strings are UTC, date-time strings without an offset are local time, and many non-ISO formats are implementation-defined. Accept strict ISO 8601 or parse with an explicit format.
Why does adding 86400000 milliseconds break tomorrow?
UTC days are always 86400000 milliseconds in JavaScript's time model, but local calendar days can be 23 or 25 hours when daylight saving time changes. Use calendar arithmetic in the target IANA timezone for local dates.
Why do date range queries miss rows at the end of the day?
Inclusive end-of-day filters such as BETWEEN '2026-06-20 00:00:00' AND '2026-06-20 23:59:59' miss fractional-second rows. Use half-open ranges: created_at >= start AND created_at < next_day_start.
Why do cron jobs behave strangely during daylight saving time?
Cron behavior varies by implementation, but local wall-clock schedules can be skipped, delayed, or handled specially when the clock jumps. For critical daily jobs, run cron in UTC or schedule outside local DST transition windows.
How do I compare two JavaScript Date objects?
Compare their numeric values, not object identity: a.getTime() === b.getTime() or +a === +b. new Date(0) === new Date(0) is false because they are different objects.
Should I use Date.now() for benchmarks?
No. Date.now() reads wall-clock time. Use performance.now() in browsers or node:perf_hooks in Node.js for elapsed-time measurement because the Performance API is monotonic and designed for high-resolution timing.
How do I find timestamp bugs in a legacy codebase?
Search for new Date( with 10-digit numbers, Date.parse, toLocaleString without timeZone, 86400000, BETWEEN with 23:59:59, Date.now in benchmark files, and Date equality comparisons. Review each match for unit, timezone, and range semantics.