skip to content

How do you convert between epoch milliseconds (a long) and Instant, and what are the precision pitfalls?

level: juniorimportance: must knowfreq 65%

answer

  1. Instant.ofEpochMilli(long) / instant.toEpochMilli()
  2. Epoch = 1970-01-01T00:00:00Z, UTC, zone-independent
  3. Instant has nanos; millis truncates sub-ms
  4. toEpochMilli can throw ArithmeticException (overflow)
  5. Use ofEpochSecond(sec, nano) for lossless

basics

~10 s

Use Instant.ofEpochMilli(long) to go from milliseconds to an Instant, and instant.toEpochMilli() to go back. The long counts milliseconds since 1970-01-01 UTC. toEpochMilli() drops sub-millisecond nanos.

solid answer

~30 s

Epoch milliseconds is a long counting milliseconds since the Unix epoch (1970-01-01T00:00:00Z). Instant.ofEpochMilli(millis) builds an Instant from it, and instant.toEpochMilli() returns it. The key pitfall is precision: Instant stores nanosecond resolution, but toEpochMilli() truncates anything finer than a millisecond, so a round-trip through a long is lossy if the Instant carried micro/nanoseconds (e.g. from a high-resolution clock). There's also a documented overflow: toEpochMilli() throws ArithmeticException for Instants far outside the long-millis range. For sub-millisecond fidelity use ofEpochSecond(seconds, nanoAdjustment) and getEpochSecond()/getNano(), or pass the Instant around directly instead of a long.

code

java · 6 lines
java
long millis = System.currentTimeMillis();
Instant instant = Instant.ofEpochMilli(millis);
long back = instant.toEpochMilli();

// Lossless path (keeps nanoseconds):
Instant exact = Instant.ofEpochSecond(instant.getEpochSecond(), instant.getNano());

go deeper

for a junior

Can call Instant.ofEpochMilli and toEpochMilli and knows epoch millis is time since 1970 UTC.

for a middle

Explains the millisecond truncation, that Instant stores nanos, and uses ofEpochSecond(sec,nano) when precision matters.

for a senior

Knows the ArithmeticException overflow, that JDK 9+ clocks can yield sub-ms precision, and designs serialization to avoid lossy round-trips.

for a principal

Sets organization-wide conventions (store Instant/ISO-8601 vs epoch millis), reasons about precision/interop tradeoffs across services and DBs, and audits for unit-mismatch bugs.

## What "epoch millis" means The **Unix epoch** is the fixed reference instant **1970-01-01T00:00:00Z** (Z = UTC). "Epoch milliseconds" is simply a `long` counting how many milliseconds have elapsed since that instant (negative for earlier times). It is the lingua franca of timestamps: `System.currentTimeMillis()`, JSON APIs, databases, and JavaScript's `Date` all use it. Because it is measured from a UTC reference, an epoch-millis value is **zone-independent** — it identifies one global moment, exactly like an `Instant`. ## The conversions ```java long millis = 1_750_000_000_000L; // long -> Instant Instant instant = Instant.ofEpochMilli(millis); // Instant -> long long back = instant.toEpochMilli(); ``` That's the whole happy path. `ofEpochMilli` splits the long into whole seconds plus a millisecond remainder internally; `toEpochMilli` reassembles them. ## Pitfall 1: precision loss An `Instant` internally stores **two fields**: `epochSecond` (a long) and `nano` (0–999,999,999), giving it **nanosecond** resolution. Milliseconds only have 3 decimal places of a second, so: - `toEpochMilli()` **truncates** anything below a millisecond. An Instant of `...:00.123456789Z` becomes `...123` millis — the `456789` nanos are silently lost. - Round-tripping `Instant -> long -> Instant` therefore **loses sub-millisecond data**. On JDK 9+, `Instant.now()` can return microsecond/nanosecond precision (depending on the OS clock), so this loss is real, not theoretical. To keep full precision, avoid the millis hop entirely — pass the `Instant` itself — or use the second+nano API: ```java long secs = instant.getEpochSecond(); int nanos = instant.getNano(); Instant exact = Instant.ofEpochSecond(secs, nanos); // lossless ``` ## Pitfall 2: overflow `Instant` can represent dates billions of years out, far beyond what a long of *milliseconds* can hold. `toEpochMilli()` throws **`ArithmeticException`** if the instant is outside the representable millis range. `ofEpochMilli` never overflows (a long of millis always fits an Instant). ## Pitfall 3: seconds vs millis confusion A frequent bug is mixing `ofEpochSecond` and `ofEpochMilli`. `Instant.ofEpochSecond(1_750_000_000_000L)` interprets the value as **seconds**, putting you ~55,000 years in the future. Always match the unit of your source data. ## Mental model Epoch millis and `Instant` are the *same idea* (a UTC moment) at *different precisions*: millis is a coarse long; Instant is a precise object. Convert freely, but remember the long is the lower-resolution container — going Instant→long→Instant can quietly round you off.

  • How would you preserve nanosecond precision when serializing an Instant?
    Either pass/serialize getEpochSecond() and getNano() (or an ISO-8601 string, which keeps nanos), or store the Instant directly; avoid the toEpochMilli() round-trip, which truncates to milliseconds.
  • Why doesn't epoch millis need a time zone?
    It counts from a fixed UTC reference (the epoch), so the same long is the same global moment everywhere; a zone is only needed to render it as a wall-clock LocalDateTime.

saying these in an interview costs you the question

  • Assuming Instant->millis->Instant is always lossless
  • Confusing ofEpochSecond with ofEpochMilli
  • Thinking epoch millis carries a time zone
  • Believing toEpochMilli can never throw

context