The boundary is one second wide and many layers deep

The largest signed 32-bit integer is 2,147,483,647. Interpreted as Unix seconds, it is:

2038-01-19T03:14:07Z

One second later, the mathematical Unix value is 2,147,483,648, which does not fit in a signed 32-bit field.

Many explanations say the value “wraps” to -2,147,483,648, producing 1901-12-13T20:45:52Z. That is a common two's complement storage result but it is not the only possible behavior. In C signed overflow is undefined, databases may not store an out of range value, serializers may truncate it, and APIs may throw an error.

The bug is not a guarantyd time jump so... It is the loss of a valid representation at a boundary that can be anywhere in the stack.

Representation Final value or range Past 2038?
signed 32-bit Unix seconds 2,147,483,647 no
unsigned 32-bit Unix seconds 4,294,967,295 until 2106 only
signed 64-bit Unix seconds about 292 billion years each side of the epoch yes for practical civil dates
JavaScript Date ±100,000,000 days from the epoch yes, within its own range
MySQL 8.4 TIMESTAMP through 2038-01-19 03:14:07 UTC no
MySQL 8.4 DATETIME through year 9999 yes, with different timezone semantics

time_t is an ABI contract, not a synonym for long

POSIX systems use time_t for calendar time, but its width has historically depended on the platform ABI. A program can run on a 64-bit kernel and still use a 32-bit userspace ABI or exchange time through a 32-bit structure.

Current POSIX requires time_t to be at least 64 bits. The GNU C Library documentation says time_t is 64 bits on its supported platforms except for a few older-platform configurations where _TIME_BITS=64 selects the 64-bit interface.

On glibc builds with support, define both macros before including system headers:

#define _FILE_OFFSET_BITS 64
#define _TIME_BITS 64

#include <stdint.h>
#include <time.h>

_Static_assert(sizeof(time_t) >= 8, "time_t must be at least 64 bits");

The file-offset macro is required with _TIME_BITS=64 on these targets. Rebuilding a single library is not enough if we have another binary component still using the old ABI.

GNU C Library: time types · GNU C Library: _TIME_BITS

Find the boundary outside the operating system

A modern time_t does not widen data that has already been packed into another type. Audit every place a timestamp crosses a boundary:

  • C and C++ structures, especially public ABI fields
  • SQL INT columns storing epoch seconds or milliseconds
  • Protocol Buffers, JSON schemas, ASN.1, and custom wire formats
  • binary file headers and filesystem metadata
  • language foreign-function interfaces
  • caches, queues, analytics events, and data-lake schemas
  • integer casts in logging, formatting, and sorting code
  • vendor firmware and devices expected to remain deployed past 2038

Search for type declarations as well as the date. A field named created_at can hide a 32-bit integer more easily than a field named created_at_epoch_seconds_i64.

MySQL TIMESTAMP still has a 2038 ceiling

MySQL 8.4 documents TIMESTAMP through 2038-01-19 03:14:07 UTC. DATETIME has a much wider calendar range, but changing types is not a mechanical substitution:

MySQL type Range concern Timezone behavior
TIMESTAMP ends in 2038 converted between session timezone and UTC
DATETIME supports years 1000–9999 stores calendar fields without timezone conversion
BIGINT epoch width depends on chosen unit and signedness application owns unit and conversion

Use DATETIME for a future civil date only when its zone semantics match the application. If you have a requirement that depends on an IANA timezone, use a timezone-aware design or a resolved instant + zone.

Find candidate columns before deciding how to migrate them:

SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM information_schema.COLUMNS
WHERE DATA_TYPE IN ('timestamp', 'int', 'integer');

An INT is only suspicious after you inspect what it stores. A TIMESTAMP is constrained by the documented range even when no row currently approaches it.

JavaScript avoids this boundary but has different limits

JavaScript Date stores milliseconds in a Number and permits values up to ±8.64e15 milliseconds from the epoch, reaching year 275760 on the positive side. It is not exposed to the signed 32-bit Unix-second boundary by itself.

It can still reintroduce Y2K38 through a cast or schema:

const boundarySeconds = 2_147_483_647;
const nextSecond = boundarySeconds + 1;

console.log(new Date(nextSecond * 1_000).toISOString());
// "2038-01-19T03:14:08.000Z"

console.log(nextSecond | 0);
// -2147483648

The bitwise operator converts the number to a signed 32-bit integer. This same narrowing can happen in native bindings, typed arrays, databases, or generated serializers.

Migrate the contract, not just the column

A reliable Y2K38 migration has several passes:

  1. Inventory every timestamp representation and record its epoch, unit, signedness, width, and timezone semantics.
  2. Widen storage and APIs to a 64-bit type or a suitable native date-time type.
  3. Version binary protocols when changing a field width alters layout.
  4. Backfill data without multiplying seconds into milliseconds twice.
  5. Dual-read or dual-write during a staged migration when old and new producers coexist.
  6. Measure how many old-format values still arrive.
  7. Remove compatibility code only after every producer and stored record has crossed the boundary.

Do not switch signed seconds to unsigned 32-bit as the long-term fix. It buys time until 2106, removes negative timestamps, and leaves the same architectural problem for the next maintainer.

Test values around the edge

Use a small boundary matrix:

Case Unix seconds Expected UTC
final signed 32-bit second 2147483647 2038-01-19T03:14:07Z
first value requiring more width 2147483648 2038-01-19T03:14:08Z
common wrapped value -2147483648 1901-12-13T20:45:52Z
final unsigned 32-bit second 4294967295 2106-02-07T06:28:15Z

For each value, test:

  • application parsing and formatting
  • database insert, query, index, backup, and restore
  • API serialization in both directions
  • message queues and event consumers
  • file and firmware upgrade paths
  • local-time display in representative zones

Favor an injected clock or explicit input to a changing shared system clock. Time travel is funny in a test name and less funny on a build host.

Boundary Date Root cause
Y2K 2000-01-01 two-digit calendar year
GPS week rollover 2019-04-06 for the second rollover 10-bit week number in legacy messages
NTP era boundary 2036-02-07 32-bit seconds field plus era interpretation
Y2K38 2038-01-19 signed 32-bit Unix seconds
unsigned Unix rollover 2106-02-07 unsigned 32-bit Unix seconds

An era-aware NTP implementation can tell eras apart while a field that does not include the era cannot. The common lesson is to record the representation, and not to assume that a common name for the timestamp makes it future proof.

The practical definition of “Y2K38 safe”

A system is safe only when every component can represent, transmit, store, compare, and format instants beyond 2038-01-19T03:14:07Z without narrowing the value or changing its meaning.

That is a property of a data contract, not a sticker on a CPU.

Frequent questions:

Q: What is the Year 2038 problem?
A: It is the failure boundary for systems that represent Unix seconds in a signed 32-bit value. The final representable second is 2038-01-19T03:14:07Z. The next second cannot fit and may wrap, fail, clamp, or trigger undefined behavior depending on the layer.
Q: What Unix timestamp marks the Year 2038 boundary?
A: 2,147,483,647 is the final signed 32-bit Unix-second value and represents 2038-01-19T03:14:07Z. The next mathematical value, 2,147,483,648, requires more than a signed 32-bit field.
Q: How do I fix the Year 2038 problem?
A: Use a 64-bit time representation at every boundary: time_t and system calls, database columns, serialized fields, file formats, and network protocols. On applicable glibc targets, compile with _TIME_BITS=64 together with _FILE_OFFSET_BITS=64.
Q: Is Y2K38 the same as Y2K?
A: No. Y2K came from storing a year with too few decimal digits. Y2K38 comes from storing elapsed Unix seconds in a signed field with too few binary bits.
Q: What is the Year 2106 problem?
A: An unsigned 32-bit Unix-second field reaches 4,294,967,295 at 2106-02-07T06:28:15Z. Using unsigned storage postpones the boundary but loses pre-epoch dates and does not solve the fixed-width design problem.
Q: Will every 64-bit system be safe?
A: No. A 64-bit operating system can still read a 32-bit database column, protocol field, file timestamp, or application integer. Safety depends on the complete data path, not the processor label.