Java and the JVM
In brief: Java itself is not affected by the Year 2038 problem. It counts time in milliseconds stored in a 64-bit number, which lasts for millions of years. The problems come from Java code that turns those values into 32-bit
intseconds, from data formats and databases that keep only 32 bits, and from the JVM's native code on 32-bit Linux. Thejava.timeAPI, available since Java 8, is the safe choice.
| Risk level | Low for the platform. Medium for applications (via int casts, legacy schemas and 32-bit JVMs) |
| Status | Core APIs are 64-bit. A known OpenJDK bug on 32-bit platforms (JDK-8279444) is open as of 2026-10-21 |
| Last reviewed | 2026-10-21 |
2038 status at a glance
- At risk ConfirmedJava int seconds and (int)(currentTimeMillis()/1000)The JVM's own time types are safe, but application int fields wrap silently.Do: Use long or java.time.Instant.
- Depends32-bit HotSpot JVM on glibc armhf / i386Native calls use the platform's time_t, and JDK-8279444 is still open.Do: Run 64-bit JVMs where possible, and test on the target platform.
Not affected or different deadline (1), and why
- Not affectedJava Date, currentTimeMillis and java.time.InstantThese are 64-bit millisecond or second counts, good for hundreds of millions of years.
Overview
From the start, Java has represented the current time as a long count of milliseconds since 1970-01-01T00:00:00Z. System.currentTimeMillis() returns a long [1], java.util.Date wraps a long, and java.time.Instant stores a long of epoch seconds plus an int of nanoseconds. That covers -1000000000-01-01T00:00Z to 1000000000-12-31T23:59:59.999999999Z [2]. A signed 64-bit millisecond counter overflows only after about 292 million years, so the platform's own clock has no 2038 limit.
Java programs still fail in 2038 for three reasons:
- Developers convert to seconds and store the result in an
int. The bug is common in caches, tokens, session stores and protocol code. Keycloak, for example, deprecated itsint-returningTime.currentTime()"to avoid integer overflow beyond year 2038" [3]. - Interop with 32-bit fields: database
TIMESTAMPcolumns, binary protocols,intcolumns in legacy schemas, and JNI code. - The JVM's native layer on 32-bit operating systems still calls C functions that use the platform's 32-bit
time_t. See Lang-C-and-CPP.
Technical details
The int seconds pitfall
// BUG: silently wraps on 2038-01-19T03:14:08Z
int expires = (int) (System.currentTimeMillis() / 1000) + ttl;
// What happens after the boundary:
long ms = Instant.parse("2038-01-19T03:14:08Z").toEpochMilli();
int bad = (int) (ms / 1000);
System.out.println(Instant.ofEpochSecond(bad)); // 1901-12-13T20:45:52Z
The (int) cast in Java never throws. It silently keeps the low 32 bits. Two fixes:
// FIX 1: keep long (or Instant) end to end
long expires = Instant.now().getEpochSecond() + ttl;
// FIX 2: if a 32-bit field truly can't change, fail loudly instead of wrapping
int wire = Math.toIntExact(epochSeconds); // throws ArithmeticException: integer overflow
Both snippets were run on OpenJDK 21 on 2026-10-21. The cast printed 1901-12-13T20:45:52Z, and Math.toIntExact threw ArithmeticException: integer overflow.
Look for these patterns:
(int) (System.currentTimeMillis() / 1000)and(int) Instant.now().getEpochSecond()intfields called*Time,*Expiry,*TimestampandcreatedByteBuffer.putInt(seconds)andDataOutputStream.writeInt(seconds)in binary protocolsInteger-typed JPA/Hibernate entity fields mapped to epoch-second columns
java.time is the fix
Instant, OffsetDateTime, ZonedDateTime and Duration are all 64-bit based. Instant.getEpochSecond() returns a long. Instant.toEpochMilli() throws ArithmeticException instead of wrapping when a value doesn't fit [2]. Prefer Instant in APIs. Convert to primitive numbers only at the edge, using a long.
JNI
jint is "signed 32 bits" and jlong is "signed 64 bits" [6]. Native methods that take or return a time should use jlong. On the C side, check the width of the platform time_t before converting:
/* BUG: jint truncates; time_t may be 32-bit on armhf/i386 */
JNIEXPORT jint JNICALL Java_Foo_now(JNIEnv *e, jclass c) { return (jint) time(NULL); }
/* FIX */
JNIEXPORT jlong JNICALL Java_Foo_now(JNIEnv *e, jclass c) { return (jlong) time(NULL); }
The JVM on 32-bit platforms
- JDK-8279444: "
File.setLastModifiedTime(long)suffers from Year 2038 problem on 32-bit platforms". On x86_32 the modification date ended up wrong (1963 instead of 2022) because of an overflow when converting for theutimescall. It affects JDK 8, 11, 17, 18 and 19, and it was still Open, fix version "tbd", when checked on 2026-10-21 [4]. - JDK-8336529: on Ubuntu 24.04 armhf, after Debian/Ubuntu's 64-bit
time_ttransition,UnixFileAttributeViews.setTimes()failed. The JDK looked upfutimenswithdlsymand got the 32-bit symbol, so a 2017 timestamp read back as 1976. The fix looks up the 64-bit variants [5].
So a 32-bit JVM is only as 2038-safe as the C library and the build flags it was compiled with. Prefer 64-bit JVMs. On 32-bit Arm, use a distribution build made with 64-bit time_t.
Kotlin and Scala
- Kotlin/JVM uses
java.timedirectly. Since Kotlin 2.3 the standard library also haskotlin.time.Instant, whoseepochSecondsis aLong[10]. TheIntpitfall is the same:(System.currentTimeMillis() / 1000).toInt()wraps silently, exactly like Java's cast. - Scala on the JVM uses
java.timeorjava.util.Date..toInton aLongtruncates in the same way.
Serialization formats used from the JVM
| Format | Time encoding | 2038-safe? |
|---|---|---|
Java serialization of Date / Instant |
long millis / long seconds + int nanos |
Yes |
Protocol Buffers google.protobuf.Timestamp |
int64 seconds + int32 nanos, valid 0001–9999 [8] |
Yes |
Apache Avro timestamp-millis / timestamp-micros |
annotate an Avro long [7] |
Yes |
Avro date |
int days since epoch [7] |
Yes (int days lasts ~5.8 million years) |
| MessagePack timestamp 32 | unsigned 32-bit seconds, range to 2106-02-07 [9] | Until 2106. Libraries switch to timestamp 64/96 automatically when needed |
Hand-rolled writeInt(seconds) |
signed 32-bit | No |
| ZIP/JAR entry times (DOS format) | 1980-based, upper limit 2107 (JDK-8161942) [11] | Until 2107 (separate limit) |
Databases matter too. MySQL's TIMESTAMP type and, before 8.0.28, UNIX_TIMESTAMP() stop at 2038-01-19 03:14:07 UTC whatever JDBC driver you use [12]. See Databases.
Affected versions / products
| Component | Representation | 2038 exposure |
|---|---|---|
System.currentTimeMillis(), java.util.Date |
long ms |
None |
java.time.Instant (Java 8+) |
long s + int ns |
None (±1 billion years) |
Application int seconds |
32-bit | High (silent wrap) |
| 32-bit HotSpot on glibc armhf/i386 | native time_t |
Medium (JDK-8279444 open) |
Keycloak Time.currentTime() |
int |
Deprecated in favour of currentTimeSeconds() (long) [3] |
Risk & compliance notes
- The JVM's reputation for being "2038-safe" can make teams skip the audit. The real exposure is in business logic, schemas and wire formats.
- Long-validity artefacts such as JWT
expclaims, refresh tokens, certificates and licence files already carry post-2038 dates. Anyintpath that handles them fails now. - Owners: application teams (casts), data/platform teams (schemas, Kafka/Avro contracts), and infrastructure (32-bit JVM deployments).
Detection & mitigation
- Static search:
grep -RnE '\(int\)\s*\(?\s*(System\.currentTimeMillis|.*getEpochSecond|.*/\s*1000)'. Also look at.toInt()in Kotlin and Scala. - Error Prone, SpotBugs or IntelliJ "narrowing conversion" inspections flag
long→intnarrowing in arithmetic contexts. Review every match that involves time. - Use
Math.toIntExactat unavoidable 32-bit boundaries so that overflow fails fast. - Test with an injected
java.time.Clockset to2038-01-19T03:14:08Zand later. That's much easier than changing the system clock. See Testing-and-Detection. - Retire 32-bit JVMs, or verify that their native layer was built with 64-bit
time_t. See Remediation-Guide.
See also
- Programming-Languages
- Lang-C-and-CPP (JNI and native
time_t) - Databases, File-Formats-and-Protocols
- OS-Linux, OS-Android
References
- System.currentTimeMillis() — Java SE 21 API, Oracle — returns
longmilliseconds since 1970-01-01 UTC. - java.time.Instant — Java SE 21 API, Oracle — MIN/MAX range,
longseconds +intnanos storage,toEpochMillithrowsArithmeticException. - org.keycloak.common.util.Time — Keycloak Javadoc —
currentTime()(int) deprecated "to avoid integer overflow beyond year 2038". - JDK-8279444: File.setLastModifiedTime(long) suffers from Year 2038 problem on 32-bit platforms — OpenJDK bug tracker, 2022-01-04 — open; affects 8–19.
- RFR: 8336529: (fs) UnixFileAttributeViews setTimes() failing on armhf, Ubuntu noble — OpenJDK nio-dev, 2024-07 — 32-bit
time_tsymbols left in glibc after the t64 transition. - JNI Types and Data Structures — Java SE 21, Oracle —
jintsigned 32 bits,jlongsigned 64 bits. - Apache Avro 1.11.1 Specification — Apache Software Foundation —
timestamp-millis/microsannotatelong;dateannotatesintdays. - Protocol Buffers well-known types: Timestamp — protobuf.dev —
int64 seconds,int32 nanos, range 0001–9999. - MessagePack specification — msgpack/msgpack on GitHub — timestamp 32 range ends 2106-02-07 06:28:16 UTC; 64/96-bit forms.
- kotlin.time.Instant — Kotlin standard library API —
epochSeconds: Long; since Kotlin 2.3. - JDK-8161942: java.util.zip.ZipEntry not covering UpperLimit range of DOS epoch — OpenJDK bug tracker — DOS time upper limit around 2107.
- Date and Time Functions — MySQL 8.0 Reference Manual —
UNIX_TIMESTAMP()/FROM_UNIXTIME()limits; 8.0.28 change on 64-bit.