7 Unix Timestamp Bugs Every Developer Has Shipped
These are the timestamp mistakes that cause production incidents: silent unit mismatches, timezone assumptions, the forgotten ×1000, string storage, ambiguous parsing, and DST boundary errors. Each bug includes the symptom, the root cause, and a practical fix.
Bug 1: Missing × 1000 in JavaScript
new Date(1700000000) produces a date near January 1970, not November 2023. JavaScript's Date constructor expects milliseconds, but most server APIs and databases return Unix seconds. This bug often appears in admin dashboards first because the backend payload is valid JSON and the UI simply renders the wrong year.
- Wrong: new Date(response.created_at) — if created_at is Unix seconds
- Right: new Date(response.created_at * 1000)
- Detection: if your date shows a year near 1970, the timestamp is in seconds not milliseconds
- Safe wrapper: const toDate = ts => new Date(ts < 1e11 ? ts * 1000 : ts)
Bug 2: Mixed units at a system boundary
JavaScript returns milliseconds. Python, Go, and most server-side APIs return seconds. When JavaScript sends a timestamp to a Python backend without converting, the backend interprets a millisecond value as seconds — placing the event in the year 55792. The reverse direction is just as bad: the UI receives seconds, sends them directly to new Date(), and displays January 1970.
- JavaScript sends: Date.now() → 1700000000000 (ms)
- Python receives: datetime.fromtimestamp(1700000000000) → year 55792
- Fix: divide by 1000 before sending to a non-JavaScript system
- Better fix: use ISO 8601 strings at API boundaries — they are unambiguous
Bug 3: Relying on the server's local timezone
Code that uses Python's datetime.fromtimestamp() without specifying UTC produces different results depending on the server's configured timezone. This works in development and breaks in production when deployed to a different region. The same category of bug appears in Node when code formats dates without an explicit timeZone option and then snapshots the result in tests.
- Wrong: datetime.fromtimestamp(ts) — uses server's local timezone
- Right: datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
- Wrong in JS: date.toLocaleDateString() — uses the Node process timezone
- Right in JS: date.toISOString() or Intl.DateTimeFormat with an explicit timeZone option
Bug 4: Storing timestamps as strings
Storing a timestamp as a formatted string in a VARCHAR column seems harmless until you need to sort by time, query a range, or do arithmetic. String comparison of dates only works with strict ISO 8601 zero-padded fields. Human-friendly strings like Nov 15, 2023 or 1/2/24 are display formats, not storage formats.
- Wrong: INSERT INTO events (created) VALUES ('Nov 15, 2023') — not sortable
- Wrong: INSERT INTO events (created) VALUES ('2023-11-15') — loses time component
- Right: use a native DATETIME/TIMESTAMPTZ column or BIGINT for Unix milliseconds
- If a string is unavoidable: use ISO 8601 with full precision: '2023-11-15T06:13:20Z'
Bug 5: Parsing ambiguous date strings
new Date('2024-01-01') and new Date('2024/01/01') look equivalent but behave differently. ISO 8601 dash format is parsed as UTC midnight; slash format is implementation-defined and most browsers parse it as local midnight. A date picker, API payload, and database row can all show the same calendar date while representing different instants.
- new Date('2024-01-01') → January 1, 2024 00:00:00 UTC (correct, explicit)
- new Date('2024/01/01') → January 1, 2024 00:00:00 local time (varies by runtime)
- new Date('January 1, 2024') → local time, may be rejected by strict parsers
- Safe rule: always use ISO 8601 with explicit timezone: new Date('2024-01-01T00:00:00Z')
Bug 6: Adding 24 hours to mean tomorrow
Adding 86,400,000 milliseconds seems like a clean way to get tomorrow, but local calendar days are not always 24 hours long. During daylight saving transitions, a local day can be 23 or 25 hours. If the product logic means next local calendar day, use calendar arithmetic in the target timezone, not duration arithmetic in UTC milliseconds.
- Wrong for calendar days: next = new Date(date.getTime() + 86400000)
- Better in local Date code: copy the Date, then call setDate(d.getDate() + 1)
- For server code: use a timezone-aware library or database date arithmetic
- For recurring schedules: store local_date, local_time, and timezone_id, then compute each occurrence
- Add tests for the DST start and DST end dates in the target timezone
Bug 7: Inclusive end-of-day filters
A common analytics query uses created_at >= start and created_at <= endOfDay. That looks reasonable, but it creates precision bugs when the database stores milliseconds, microseconds, or nanoseconds. The safer pattern is a half-open range: greater than or equal to the start, and strictly less than the next period's start.
- Risky: WHERE created_at <= '2026-05-19 23:59:59'
- Safer: WHERE created_at >= '2026-05-19' AND created_at < '2026-05-20'
- Half-open ranges work for seconds, milliseconds, microseconds, and nanoseconds
- The same pattern works for months: created_at >= monthStart AND created_at < nextMonthStart
Production prevention checklist
Most timestamp incidents are preventable with naming, tests, and explicit boundaries. The goal is not to make every line of date code clever; it is to make the unit, timezone, and range semantics boringly obvious.
- Name numeric fields with units: createdAtMs, expiresAtSeconds, imported_at_ms
- Use UTC for storage and explicit IANA timezones for display
- Prefer ISO 8601 strings with Z or offsets at human-inspected API boundaries
- Use half-open ranges for date filters
- Test around January 1, leap days, DST transitions, and the Year 2038 boundary
Unix timestamp bugs FAQ
- What is the most common Unix timestamp bug?
- Passing seconds where milliseconds are expected, or the reverse. In JavaScript, new Date(unixSeconds) lands in 1970 because the constructor expects milliseconds — multiply the 10-digit value by 1000 first.
- How do I prevent seconds-vs-milliseconds bugs?
- Name fields with their unit (createdAtMs, expiresAtSeconds), convert at system boundaries, and prefer ISO 8601 strings in API contracts so the value is self-describing.
- Why does adding 86,400,000 ms break 'tomorrow'?
- Local calendar days are 23 or 25 hours long during daylight saving transitions, so adding a fixed 24 hours can land on the wrong date. Use calendar arithmetic in the target timezone instead.