How to Get the Current Epoch Timestamp in Java
Java 13 min read
Seconds against milliseconds is the bug this causes, why an int epoch stops working in 2038, and the fact that a LocalDateTime cannot be converted without supplying a zone.
The Unix epoch timestamp is the number of seconds since 1970-01-01T00:00:00Z. Java’s methods return milliseconds by default, which is a factor of a thousand between two things that both look like plausible timestamps, and the resulting dates land in 1970 or in the year 56000 rather than throwing.
Written against Java 17.
Getting it
long seconds = Instant.now().getEpochSecond(); // 1787654400
long millis = Instant.now().toEpochMilli(); // 1787654400123
long alsoMs = System.currentTimeMillis(); // 1787654400123
Instant.now().getEpochSecond() is the Unix timestamp as most APIs mean it. toEpochMilli() and
System.currentTimeMillis() are the same moment in milliseconds.
Instant is UTC by definition, so none of these depend on the JVM’s time zone. That is what makes an
epoch timestamp unambiguous and why it is the right thing to store and transmit.
For sub-second precision:
Instant now = Instant.now();
long micros = ChronoUnit.MICROS.between(Instant.EPOCH, now);
long nanos = ChronoUnit.NANOS.between(Instant.EPOCH, now);
int nanoOfSecond = now.getNano(); // the fraction within the current second, 0–999,999,999
getNano() is the fractional part, not a nanosecond timestamp. Adding it to getEpochSecond()
without multiplying is a common slip.
The precision you actually get is a platform property rather than a promise of the type. On Java 9
and later Instant.now() typically resolves to microseconds on Linux and to something coarser on
Windows, so two instants captured in quick succession can be equal. Ordering events by timestamp
alone is therefore unreliable: add a sequence number when the order must be total.
Which unit does the other system want?
Ten digits is seconds; thirteen is milliseconds. That is the fastest check, and it holds until the year 2286.
| Value | Unit | Reads as |
|---|---|---|
1787654400 | seconds | 2026-08-26 |
1787654400000 | milliseconds | 2026-08-26 |
1787654400 read as millis | — | 1970-01-21 |
1787654400000 read as seconds | — | year 56,600 |
Unix tools, JWT exp and iat, most Linux APIs and Postgres EXTRACT(EPOCH FROM ...) use
seconds. JavaScript’s Date.now(), Java’s own System.currentTimeMillis() and most JVM
libraries use milliseconds.
Name the variable for the unit — expiresAtSeconds, not expiresAt. A bare long carries no unit
and the compiler cannot help.
Converting back
Instant fromSeconds = Instant.ofEpochSecond(1787654400L);
Instant fromMillis = Instant.ofEpochMilli(1787654400123L);
ZonedDateTime local = fromSeconds.atZone(ZoneId.of("Europe/Berlin"));
LocalDate date = local.toLocalDate();
Instant.ofEpochSecond also takes a nanosecond adjustment as a second argument, which is how to
reconstitute a value split across two fields. Both factory methods accept negative values, which
represent moments before 1970 — worth knowing because a zero or a negative timestamp in production
data is usually an uninitialised field rather than a date in 1969.
From a date to an epoch
An Instant converts directly. A LocalDateTime does not:
Instant instant = Instant.now();
long a = instant.getEpochSecond();
ZonedDateTime zoned = ZonedDateTime.now();
long b = zoned.toEpochSecond();
LocalDateTime local = LocalDateTime.now();
long c = local.toEpochSecond(ZoneOffset.UTC); // requires an offset — and which one?
long d = local.atZone(ZoneId.of("Europe/Berlin")).toEpochSecond();
A LocalDateTime is a wall-clock reading with no zone, so it does not identify a moment. Turning it
into an epoch requires saying where that clock was, and ZoneOffset.UTC is a choice rather than a
neutral default — it is wrong by an hour or two for a value that came from a local clock.
LocalDate needs a time as well:
long startOfDay = LocalDate.now().atStartOfDay(ZoneId.of("Europe/Berlin")).toEpochSecond();
atStartOfDay(zone) rather than atTime(0, 0) — on the few daylight-saving nights where midnight
does not exist locally, the first returns the first valid moment and the second does not.
Where the unit mismatch actually shows up
Three boundaries account for nearly all of it, and each has a tell.
JWT claims. exp, iat and nbf are seconds by specification. Building a token with
System.currentTimeMillis() + 3600_000 produces an expiry roughly fifty thousand years out, and
every validator accepts it because it is a valid future timestamp. The tell is that the token never
expires in testing.
long exp = Instant.now().plus(Duration.ofHours(1)).getEpochSecond(); // correct
Databases. Postgres to_timestamp() and MySQL UNIX_TIMESTAMP() are seconds; a value stored
from Java is usually milliseconds. A column populated by both application code and a SQL default ends
up holding two units, and nothing detects it — the rows sort correctly among themselves and wrongly
against each other.
JavaScript. new Date(epoch) takes milliseconds. Sending seconds to a browser produces a date in
January 1970, which at least fails visibly.
A defence that costs nothing is a sanity check at a boundary you do not control:
static Instant fromEpoch(long value) {
// anything past the year 3000 in seconds is almost certainly milliseconds
return value > 32_503_680_000L ? Instant.ofEpochMilli(value) : Instant.ofEpochSecond(value);
}
That is a heuristic, not a substitute for agreeing the unit with a system you do control.
The 2038 problem
int bad = (int) Instant.now().getEpochSecond(); // fine today, broken in 2038
A signed 32-bit second count overflows on 19 January 2038, wrapping to 1901. Java’s long is not
affected, and the exposure is at the boundaries: a database INT column, a C API, a protocol field,
or a cast written to satisfy a signature.
Milliseconds in an int overflow far sooner — 24 days from the epoch — so that cast is broken
already.
The cast is the part worth grepping for. (int) on a timestamp compiles silently and is invisible in
review, and the overflow is not an exception — the value wraps negative and reads as a date in 1901.
A test written today passes; the failure is scheduled.
Keep epoch values in long and use BIGINT in the database.
Storing and comparing
An epoch timestamp is a good wire and storage format: unambiguous, sortable as a number, and compact. It has two costs worth stating.
It is unreadable to a person, so a log line or a database row cannot be checked by eye — a
TIMESTAMPTZ column is easier to work with, and the epoch is better in a JSON payload.
And it identifies a moment, not a calendar date. Anything answering “which day did this happen” needs a zone applied first; a query grouping epoch values by day in UTC quietly reports a different day for anyone east or west of it.
A related decision is what to store when the value is a future deadline rather than a record of something that happened. An epoch is right for “this token expires at this moment”. It is wrong for “this subscription renews on the 1st of each month”, because a calendar rule is not a moment and resolving it to one freezes a daylight-saving offset that will be wrong later. Store the rule, and compute the moment when you need it.
Comparison is ordinary numeric comparison, with the usual caveat:
if (Instant.now().getEpochSecond() > token.expiresAtSeconds()) { }
Both sides must be the same unit. Mixing them produces a token that never expires or one that is always expired, and only one of those is noticed quickly.
Elapsed time is a different question
long start = System.nanoTime();
doWork();
Duration elapsed = Duration.ofNanos(System.nanoTime() - start);
System.currentTimeMillis() and Instant.now() read the wall clock, which is adjustable — an NTP
correction during the measurement can make the difference negative. System.nanoTime() is monotonic
and meaningless as an absolute value, which is exactly right for a duration.
Related: getting the current date and time, comparing and formatting. More in the Java guides.
Frequently asked questions
How do I get the Unix timestamp in Java?
Instant.now().getEpochSecond(). That is seconds, which
is what “Unix timestamp” normally means.
Seconds or milliseconds?
getEpochSecond() gives seconds; toEpochMilli() and
System.currentTimeMillis() give milliseconds. Ten digits is seconds, thirteen is milliseconds.
Which do other systems expect?
Unix tools, JWT claims and Postgres EXTRACT(EPOCH ...) use
seconds. JavaScript’s Date.now() and most JVM libraries use milliseconds. Put the unit in the
variable name.
Does the epoch depend on the time zone?
No. It counts from a fixed UTC moment, which is what makes it unambiguous and the right thing to store.
How do I convert a LocalDateTime to an epoch?
You cannot without a zone — it is a wall-clock
reading, not a moment. Use atZone(zone).toEpochSecond(), and treat ZoneOffset.UTC as a decision.
What is getNano()?
The fraction within the current second, 0 to 999,999,999. It is not a nanosecond timestamp, and adding it to the epoch seconds without scaling is wrong.
What is the 2038 problem?
A signed 32-bit second count overflows on 19 January 2038 and wraps to
1901. Java’s long is safe; the risk is an int cast, an INT column, or a C API at the boundary.
Can I store an epoch in an INT column?
No. Use BIGINT. Seconds break in 2038 and milliseconds
broke 24 days after the epoch.
How do I get the epoch for the start of today?
LocalDate.now().atStartOfDay(zone).toEpochSecond().
Use atStartOfDay, not atTime(0, 0) — midnight does not exist on some daylight-saving nights.
Should I use an epoch to measure elapsed time?
No. The wall clock is adjustable and can move
backwards. Use System.nanoTime(), which is monotonic.