Unixdates

Guides · 8 min read

The Year 2038 Problem (Y2K38), explained

On 19 January 2038 at 03:14:07 UTC, every signed 32-bit Unix timestamp in the world overflows. Here is what actually happens, where it still matters, and how to find and fix it in your own code.

Published 22 April 2026

A lot of people heard about Y2K, lived through the night of 31 December 1999 with nothing breaking, and concluded that the whole “digital apocalypse” thing was overblown. Y2K38 is the sequel, except it is built into the C standard library, has been ticking down for 50 years, and is much harder to fix in a single rewrite.

The good news: most modern software has already dodged it. The bad news: there is a long tail of older systems that haven’t.

This guide covers what the problem actually is, where it is still lurking, and how to handle it in code you ship today.

What overflows, exactly

Classical Unix systems store a timestamp in a time_t value, which on older platforms was a signed 32-bit integer. A signed 32-bit integer can represent values from -2,147,483,648 to 2,147,483,647.

That maximum value, when interpreted as seconds since 1970-01-01 UTC, lands on:

2038-01-19 03:14:07 UTC

One second later, the counter overflows. In two’s complement arithmetic, the value wraps to -2,147,483,648, which decodes as 1901-12-13 20:45:52 UTC.

So a system running on signed 32-bit time goes from “Tuesday morning in 2038” straight to “Friday evening in 1901”. Anything that compares timestamps, sorts records by time, schedules tasks, or expires sessions starts producing nonsense — sometimes silently, sometimes with crashes, sometimes by deleting things it thinks are 137 years stale.

You can see this for yourself in C:

#include <stdio.h>
#include <time.h>

int main() {
    time_t t = 2147483647;       // last good second
    printf("ok:    %s", ctime(&t));
    t += 1;                      // overflow
    printf("after: %s", ctime(&t));
    return 0;
}

On a 32-bit time_t build, after: prints a date in 1901.

Why 32 bits in the first place

When Unix was designed in the early 1970s, 32-bit integers were the natural word size on the machines it ran on. A 32-bit time_t gave you 68 years either side of the epoch — comfortably more than the lifetime of any computer at the time.

This was a perfectly reasonable choice in 1971. It became a problem when Unix turned out to outlive the architectures it was designed for, by a lot.

What’s already been fixed

Most large-scale modern software is already 64-bit safe:

  • Linux, on every architecture except 32-bit ones with _TIME_BITS=32, uses a 64-bit time_t. Glibc 2.34 and the kernel together added Y2038-safe variants of all the time-related syscalls; distributions started flipping the default in 2022–2024.
  • macOS is 64-bit only since Catalina (2019). Y2038 is not a concern.
  • Windows uses its own time formats but time_t in MSVC has been 64-bit by default since Visual Studio 2005.
  • Java uses a 64-bit long in Instant and Date, so Y2038 doesn’t bite.
  • Go uses 64-bit nanoseconds since 1885 internally.
  • Python uses arbitrary-precision integers for time.time()’s integer form and floats for the default — both are fine well past 2038.
  • JavaScript uses milliseconds-since-epoch in a 64-bit double. Safe.

What’s still vulnerable

The danger is not your laptop. It is everywhere code from the 1990s and 2000s is still running:

Embedded systems and IoT. Network routers, smart meters, industrial controllers, payment terminals, and the firmware on countless cheap SBCs still use 32-bit time_t and won’t be patched. Anything with a “shipped, never to be updated” lifecycle is a candidate.

32-bit application binaries. A binary compiled against a 32-bit Linux libc on Debian 11 or older still has a 32-bit time_t even if it runs on a 64-bit kernel. Some legacy enterprise software falls into this bucket.

Old database columns. A INT column (not BIGINT) used to hold Unix seconds will overflow. Same for any home-rolled binary timestamp format that uses 4 bytes.

Filesystems. ext3 files have 32-bit timestamps. ext4 has 34 bits and runs out in 2446. UFS1, original NFSv2, and a handful of others have the 2038 problem. ext4 with the inode_size ≥ 256 mitigation, btrfs, xfs, and zfs are all fine.

Old protocols. Original NFSv2, certain DNS extensions, and some industrial control protocols specify a 32-bit Unix time field on the wire. Even if both endpoints are 64-bit internally, the protocol pinches the value down.

How to audit your own code

The two most common offenders in modern application code are:

1. Database columns using 32-bit integer types.

-- Bad: overflows in 2038.
created_at INTEGER

-- Good.
created_at BIGINT
-- Or, even better:
created_at TIMESTAMPTZ

If you’re on Postgres and want to be safe, prefer TIMESTAMPTZ over a raw integer column unless you have a specific reason. Postgres stores it as a 64-bit microsecond count internally — well past 2038 — and you get type safety, indexing, and timezone-aware comparisons for free.

2. Code paths that round-trip through a 32-bit signed integer.

Watch for cases where you cast a Unix timestamp to a int32, i32, or use a wire protocol that does. A common pattern is hashing a timestamp into a 32-bit slot for cache keys; that’s fine for the hash, but make sure the original value is wider.

// Suspicious — 32-bit truncation.
var t int32 = int32(time.Now().Unix())

// Fine.
t := time.Now().Unix() // returns int64

In TypeScript / JavaScript, Date and Date.now() use 64-bit doubles for milliseconds, which is safe through about year 285,000. The only place to look is when you serialize over a wire protocol that pins the value to 32 bits.

Don’t rely on Math.floor(...) | 0

JavaScript developers occasionally use | 0 or >>> 0 to coerce a number into an integer. Both of these truncate to 32 bits. If you do this with a Unix-seconds timestamp, you will start producing wrong values today, not in 2038, because timestamps in seconds have already been past 2,147,483,647 since January 2038 in tests but will only matter in production from then on. (Ten-digit timestamps like 1745301600 are well below the 32-bit limit, so | 0 happens to work — until it suddenly doesn’t.)

Better:

// Don't.
const t = Math.floor(Date.now() / 1000) | 0;

// Do.
const t = Math.floor(Date.now() / 1000);

What to actually do about it

For most application developers, the practical checklist is short:

  1. Use BIGINT (or your DB’s equivalent) for any column that stores Unix seconds, or use a real timestamp type.
  2. Use 64-bit integer types in your code (int64, i64, BigInt if you’re outside JS’s safe-integer range — but you aren’t, since Number safely holds milliseconds well past year 200,000).
  3. Audit any old binary protocols, file formats, or interop code you maintain.
  4. If you ship firmware: build with a 64-bit time_t and test that your build system actually does it.

If you want to verify your understanding by playing with the actual numbers, paste 2147483647 into the Unixdates converter and you’ll see the last second of 32-bit time rendered out as a real date.

Need to convert a timestamp right now? Try the Unixdates converter — auto-detects seconds, milliseconds and microseconds.