Time

kotlin.time.Duration, measureTime, kotlinx-datetime, and interop with java.time.

Duration measureTime Instant LocalDateTime TimeZone java.time
โฑ

kotlin.time.Duration

Duration represents a span of time โ€” not a point in time โ€” and it exists specifically to stop bugs caused by an unlabeled Long that might mean milliseconds, seconds, or nanoseconds depending on which API you're calling. It's a value class under the hood, so wrapping a number this way costs essentially nothing at runtime while making unit mismatches a compile-time impossibility.

Building and comparing durations Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes

// Extension properties on Int/Long build a Duration directly โ€”
// the unit is part of the value, not a separate parameter to misread
val timeout = 30.seconds
val pollInterval = 500.milliseconds
val sessionLength = 15.minutes

// Arithmetic and comparison work directly, unit conversion is automatic
val total = timeout + pollInterval
println(total)               // 30.5s
println(total < sessionLength)  // true

// Convert to a specific unit only when an external API demands a raw number
println(timeout.inWholeMilliseconds)  // 30000
๐Ÿน
Coming from Go: Duration maps directly onto Go's time.Duration โ€” both are a typed wrapper around a raw number specifically to prevent "was that milliseconds or seconds?" bugs, and both offer unit-labeled constructors (30 * time.Second vs Kotlin's 30.seconds).
โฒ

measureTime & measureTimedValue

Timing a block of code by hand means capturing System.nanoTime() before and after and subtracting โ€” easy to get slightly wrong, and it returns a raw Long with no unit attached. measureTime wraps that pattern and returns a proper Duration directly; measureTimedValue does the same but also keeps the block's actual return value alongside the timing.

Timing a computation measureTime
import kotlin.time.measureTime
import kotlin.time.measureTimedValue

// measureTime: just the duration, block's result discarded
val elapsed = measureTime {
    (1..1_000_000).sum()
}
println("took $elapsed")  // e.g. "took 8.2ms"

// measureTimedValue: both the duration AND the block's result
val (result, duration) = measureTimedValue {
    (1..1_000_000).sum()
}
println("result=$result, took=$duration")
๐Ÿ“…

kotlinx-datetime: Instant & LocalDateTime

kotlinx-datetime is the official multiplatform date/time library โ€” needed because java.time only exists on the JVM, and Kotlin/JS and Kotlin/Native have nothing equivalent built in. Its central distinction mirrors java.time's own: Instant is an unambiguous point on the global timeline (nanoseconds since the epoch), while LocalDateTime is "civil time" โ€” a date and time with no attached time zone, the kind of value a calendar app shows you.

Instant: an unambiguous moment Instant
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

// Clock.System.now() returns the current Instant โ€” same moment
// everywhere on Earth, regardless of the caller's time zone
val now: Instant = Clock.System.now()

val later = now + kotlin.time.Duration.parse("1h")
println(later > now)  // true: Instants compare directly, no ambiguity

val parsed = Instant.parse("2024-03-15T10:30:00Z")  // ISO-8601, "Z" = UTC
LocalDateTime: civil time, no time zone LocalDateTime
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone

// "March 15th, 2024, 10:30 AM" โ€” meaningless without a time zone
// to say WHERE that clock reading applies
val civilTime = LocalDateTime(2024, 3, 15, 10, 30)
println(civilTime)  // 2024-03-15T10:30

// An Instant only becomes civil time once you attach a zone
val now = kotlinx.datetime.Clock.System.now()
val localNow = now.toLocalDateTime(TimeZone.currentSystemDefault())
println(localNow)
๐Ÿ’ก
Store and compare timestamps as Instant; only convert to LocalDateTime at the point where you're displaying a value to a human in a specific place. Mixing the two โ€” comparing a LocalDateTime from one zone against one from another as if they were on the same timeline โ€” is a recurring source of off-by-hours bugs in date/time code across every language, not just Kotlin.
๐ŸŒ

TimeZone

TimeZone is what converts between an unambiguous Instant and a zone-specific LocalDateTime, in both directions. It's identified by IANA database names like "America/New_York" โ€” never by a fixed UTC offset alone, since a named zone accounts for daylight saving transitions that a plain offset can't.

Converting between Instant and a specific zone TimeZone
import kotlinx.datetime.*

val now = Clock.System.now()

val tokyo = TimeZone.of("Asia/Tokyo")
val newYork = TimeZone.of("America/New_York")

// Same Instant, different LocalDateTime depending on the zone
println(now.toLocalDateTime(tokyo))
println(now.toLocalDateTime(newYork))

// The reverse: interpret civil time AS a specific zone, get back an Instant
val meetingTime = LocalDateTime(2024, 3, 15, 9, 0)
val meetingInstant = meetingTime.toInstant(newYork)
โ˜•

java.time Interop

On the JVM specifically, plenty of existing libraries (JDBC drivers, older APIs) still speak java.time types directly. kotlinx-datetime provides explicit conversion functions rather than making its own types secretly interchangeable with Java's, which keeps the conversion visible in the code wherever it happens.

Converting at a JVM interop boundary toJavaInstant / toKotlinInstant
import kotlinx.datetime.Clock
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toKotlinInstant
import java.time.Instant as JavaInstant

val kotlinNow = Clock.System.now()

// kotlinx.datetime.Instant -> java.time.Instant, for a Java-facing API
val javaInstant: JavaInstant = kotlinNow.toJavaInstant()

// java.time.Instant -> kotlinx.datetime.Instant, coming back the other way
val backToKotlin = javaInstant.toKotlinInstant()
โ„น๏ธ
For a pure JVM-only project with no plans to target Kotlin/JS or Kotlin/Native, using java.time directly (via ordinary Java interop, no special Kotlin wrapping needed) is a perfectly reasonable choice and avoids an extra dependency. Reach for kotlinx-datetime specifically when multiplatform support matters, or when its API ergonomics (like Duration integration) are worth the added dependency on a JVM-only project too.
๐Ÿ“‹

Quick Reference

Concept Syntax Notes
Build a Duration30.seconds / 500.millisecondsUnit is part of the value, not a separate parameter
Convert to raw unitsduration.inWholeMillisecondsOnly needed at an API boundary demanding a raw number
Time a blockmeasureTime { ... }Returns a Duration directly
Time + keep the resultmeasureTimedValue { ... }Returns (value, duration)
Unambiguous momentClock.System.now(): InstantSame point on the timeline everywhere
Civil time, no zoneLocalDateTime(y, m, d, h, min)Meaningless without a TimeZone attached
Instant โ†’ civil timeinstant.toLocalDateTime(zone)Requires an explicit TimeZone
Civil time โ†’ InstantlocalDateTime.toInstant(zone)Interprets the civil time as being in that zone
Named time zoneTimeZone.of("America/New_York")IANA name, not a fixed UTC offset
JVM interopkotlinInstant.toJavaInstant()Explicit conversion, not implicit interchangeability