nicetool.dev logo
#time#unix#backend

Working with Unix Timestamps: Seconds vs Milliseconds, Time Zones and the 2038 Overflow

What a Unix timestamp is, seconds vs milliseconds, converting timestamps in JavaScript, Python and SQL, time zone best practices, and how the Year 2038 problem can still break your software.

3 min read

Almost every system you build stores time somewhere: log entries, orders, sessions, token expiry. Under the hood, most of them rely on the Unix timestamp. It looks like a simple number, but it causes a surprising number of bugs, from off-by-1000 errors to a looming overflow in January 2038.

What is a Unix timestamp?

A Unix timestamp (also called Unix time, POSIX time or epoch time) is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970, the "Unix epoch".

0           → 1970-01-01 00:00:00 UTC
1700000000  → 2023-11-14 22:13:20 UTC
2147483647  → 2038-01-19 03:14:07 UTC

Its advantages are why it is everywhere:

  • Time-zone independent. The same instant has the same timestamp in Hanoi and New York.
  • Easy to compare and calculate. "Is this token expired?" is just now > exp. "One day later" is + 86400.
  • Compact. A single integer is cheap to store, index and send over the network.

You can convert any timestamp to a human-readable date (and back) with our Unix Timestamp Converter.

Seconds or milliseconds?

The most common timestamp bug is mixing up units:

Digits Unit Typical source
10 (e.g. 1700000000) Seconds Unix/Linux, PHP, Python time.time() (as float), JWT exp/iat, most APIs
13 (e.g. 1700000000000) Milliseconds JavaScript Date.now(), Java System.currentTimeMillis()
16 / 19 Micro / nanoseconds Databases, Go time.UnixNano(), tracing systems

If a date shows up as January 1970, you probably passed seconds where milliseconds were expected. If it shows a year far in the future like 55,000, you did the opposite.

JavaScript

// current time
const seconds = Math.floor(Date.now() / 1000);

// timestamp → Date (note the * 1000)
const date = new Date(1700000000 * 1000);
date.toISOString(); // "2023-11-14T22:13:20.000Z"

Python

from datetime import datetime, timezone

ts = int(datetime.now(timezone.utc).timestamp())
datetime.fromtimestamp(1700000000, tz=timezone.utc)
# datetime.datetime(2023, 11, 14, 22, 13, 20, tzinfo=datetime.timezone.utc)

SQL

-- PostgreSQL
SELECT to_timestamp(1700000000);            -- timestamptz
SELECT extract(epoch FROM now())::bigint;

-- MySQL
SELECT FROM_UNIXTIME(1700000000);
SELECT UNIX_TIMESTAMP();

Shell

date +%s                 # current timestamp
date -u -d @1700000000   # GNU date: Tue Nov 14 22:13:20 UTC 2023

Time zone best practices

A timestamp has no time zone. Problems appear when you convert it to a local date:

  1. Store instants in UTC (as a timestamp or a timestamptz column) and convert to local time only when displaying.
  2. Send ISO 8601 strings with an offset in APIs, e.g. 2023-11-14T22:13:20Z or 2023-11-15T05:13:20+07:00, if you do not use numeric timestamps.
  3. Store the user's time zone name (for example Asia/Ho_Chi_Minh), not just an offset, when you need to schedule future events. Offsets change with daylight saving time and political decisions.
  4. Remember scheduled jobs run in the server's zone. A cron job on a UTC server that should run at 09:00 in Vietnam needs 0 2 * * *. Check your schedule with the Cron Expression Parser.

Unix time also ignores leap seconds: every day is treated as exactly 86,400 seconds. For almost all applications this is exactly what you want.

The Year 2038 problem

Historically, many systems stored Unix time in a signed 32-bit integer (time_t on 32-bit platforms). The largest value such an integer can hold is:

2^31 − 1 = 2,147,483,647  →  2038-01-19 03:14:07 UTC

One second later the value overflows and wraps around to −2,147,483,648, which is interpreted as 13 December 1901, 20:45:52 UTC. Software that is not prepared may suddenly think certificates are not yet valid, reject logins, or compute negative durations.

Is it still a real risk?

Modern 64-bit operating systems use a 64-bit time_t, which lasts for about 292 billion years. Linux has also added 64-bit time support for 32-bit architectures (kernel 5.6 and glibc 2.34). But 2038 can still bite in places such as:

  • Embedded devices and IoT running old 32-bit firmware for decades: routers, cars, industrial controllers, medical equipment.
  • Database columns: the MySQL TIMESTAMP column type is still limited to 2038-01-19 03:14:07 UTC. Use DATETIME or a BIGINT instead for dates that may go beyond that.
  • File formats and protocols with 32-bit time fields.
  • Application code that casts timestamps to 32-bit integers, for example int32 fields in Protocol Buffers or INT columns.

And 2038 is not that far away. A 15-year mortgage or a long-lived certificate created today already has an end date after it.

How to prepare

  • Use 64-bit integers (BIGINT, int64) for timestamps everywhere.
  • Avoid MySQL TIMESTAMP for future dates.
  • Test your system with dates after 2038, for example by generating an expiry of 2147483648 and checking what happens.
  • Audit embedded and third-party components for 32-bit time handling.

Conclusion

Unix timestamps are simple and powerful: one integer, one universal instant. To avoid bugs, always know which unit (seconds or milliseconds) you are handling, store in UTC, convert to local time only for display, and make sure nothing in your stack squeezes time into 32 bits.

Need to decode a timestamp from a log or a JWT right now? Our free Unix Timestamp Converter converts seconds and milliseconds to dates in any time zone, directly in your browser.

Written by

Nguyễn Thành Nam

Full-Stack Engineer · Backend & Platform

Full-stack engineer with 9+ years of experience building high-traffic APIs and platforms with Go, NestJS, PHP (Laravel, Phalcon) and Node.js. Specializes in GraphQL API design, real-time systems and performance optimization, and builds NICETOOL.dev for the developer community.

Try it yourself

Related articles