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
INTcolumns 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:
- Inventory every timestamp representation and record its epoch, unit, signedness, width, and timezone semantics.
- Widen storage and APIs to a 64-bit type or a suitable native date-time type.
- Version binary protocols when changing a field width alters layout.
- Backfill data without multiplying seconds into milliseconds twice.
- Dual-read or dual-write during a staged migration when old and new producers coexist.
- Measure how many old-format values still arrive.
- 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.
Related rollovers are not identical bugs
| 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.
Related reading
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.