Duration #
Working with time spans — how long an operation ran, how long to wait before retrying, how much time remains in a session — is a very common need. Before Kotlin 1.6, developers had to work directly with milliseconds or seconds as Long, which is prone to unit bugs (forgetting to multiply by 1000, or using the wrong unit). Kotlin introduced kotlin.time.Duration as a value type that represents time durations in a type-safe way: 5.seconds, 30.minutes, 2.hours — all with the same type and all operable on each other. Combined with measureTime and measureTimedValue for benchmarking, Duration becomes a clean foundation for all measurement and time management needs. This article covers the entire Kotlin Duration API, from creation to use in coroutines and performance profiling.
Creating Durations #
Duration can be created from numbers using extension properties available for Int, Long, and Double.
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
// Extension properties for creating Durations
val limaDetik = 5.seconds
val tigaPuluhMenit = 30.minutes
val duaJam = 2.hours
val satuHari = 1.days
val seratusMs = 100.milliseconds
val seribuUs = 1000.microseconds
val sejutaNs = 1_000_000.nanoseconds
// Double is also supported
val setengahDetik = 0.5.seconds // 500ms
val satuSetengahJam = 1.5.hours // 90 minutes
val duaSetengahHari = 2.5.days // 60 hours
// From Long
val timeout: Duration = 30L.seconds
val ttl: Duration = 24L.hours
// Duration.ZERO and INFINITE
val nol = Duration.ZERO
val tak_terbatas = Duration.INFINITE
println(limaDetik) // 5s
println(tigaPuluhMenit) // 30m
println(duaJam) // 2h
println(seratusMs) // 100ms
println(setengahDetik) // 500ms
flowchart LR
A["Numbers"] --> B["Extension Properties"]
B --> C["Int / Long / Double"]
C --> D[".nanoseconds"]
C --> E[".microseconds"]
C --> F[".milliseconds"]
C --> G[".seconds"]
C --> H[".minutes"]
C --> I[".hours"]
C --> J[".days"]
D & E & F & G & H & I & J --> K["Duration\n(single type)"]Converting Between Units #
Duration stores its value internally in nanoseconds with high precision. Converting to other units is easy and expressive.
val durasi = 90.minutes
// Convert to Long
println(durasi.inWholeSeconds) // 5400 (Long)
println(durasi.inWholeMinutes) // 90 (Long)
println(durasi.inWholeHours) // 1 (Long — rounded down)
println(durasi.inWholeDays) // 0 (Long)
println(durasi.inWholeMilliseconds) // 5_400_000 (Long)
println(durasi.inWholeNanoseconds) // 5_400_000_000_000 (Long)
// Convert to Double (preserves fractions)
println(durasi.inSeconds) // 5400.0 (Double)
println(durasi.inMinutes) // 90.0 (Double)
println(durasi.inHours) // 1.5 (Double)
println(durasi.inDays) // 0.0625 (Double)
// toComponents — split into components
durasi.toComponents { jam, menit, detik, nanosecond ->
println("$jam hours $menit minutes $detik seconds")
// "1 hours 30 minutes 0 seconds"
}
// Example: a 99.9% SLA uptime in a year
val setahun = 365.days
val sla999 = setahun * 0.001 // the allowed 0.1% downtime
sla999.toComponents { jam, menit, _, _ ->
println("Maximum downtime: $jam hours $menit minutes per year")
// "8 hours 45 minutes"
}
Parsing from Strings #
// Duration.parse — reads from ISO 8601 or Kotlin format strings
val d1 = Duration.parse("PT5M") // 5 minutes (ISO 8601)
val d2 = Duration.parse("PT1H30M") // 1 hour 30 minutes
val d3 = Duration.parse("5m") // 5 minutes (Kotlin format)
val d4 = Duration.parse("1h 30m") // 1 hour 30 minutes
val d5 = Duration.parse("2d 3h 15m 30s") // 2 days 3 hours 15 minutes 30 seconds
// parseOrNull — safe for untrusted input
val valid = Duration.parseOrNull("30s") // 30s
val invalid = Duration.parseOrNull("xyz") // null
// Parsing from milliseconds (from a database or API)
val fromMs = 5_400_000L.milliseconds // from a long millisecond value
val fromSeconds = 5400.seconds // from a long second value (Unix timestamp diff)
Arithmetic Operations #
Duration supports intuitive mathematical operations.
val satuJam = 1.hours
val tigaPuluhMenit = 30.minutes
val limaDetik = 5.seconds
// Addition and subtraction
val satuSetengahJam = satuJam + tigaPuluhMenit // 1h 30m
val setengahJam = satuJam - tigaPuluhMenit // 30m
val selisih = satuJam - 90.minutes // Duration.ZERO? — can be negative!
// Durations can be negative
val negatif = 30.minutes - 1.hours // -30m
println(negatif.isNegative()) // true
println(negatif.absoluteValue) // 30m
// Multiplication and division by a scalar
val tigaJam = satuJam * 3 // 3h
val duapuluhMenit = satuJam / 3 // 20m
val duaKali = limaDetik * 2.0 // 10s
val setengah = satuJam * 0.5 // 30m
// Dividing a Duration by a Duration — produces a Double
val rasio = satuSetengahJam / satuJam // 1.5
// Comparison
println(satuJam > tigaPuluhMenit) // true
println(limaDetik < tigaPuluhMenit) // true
println(satuJam == 60.minutes) // true
println(satuJam.compareTo(90.minutes)) // negative (less than)
// Other useful operations
println(satuJam.coerceAtMost(30.minutes)) // 30m
println(limaDetik.coerceAtLeast(1.minutes)) // 1m
// Checks
println(Duration.ZERO.isPositive()) // false
println(5.seconds.isPositive()) // true
println((-5).seconds.isNegative()) // true
println(Duration.INFINITE.isInfinite()) // true
measureTime — Measuring Execution Time #
measureTime executes a code block and returns a Duration representing the time it took.
import kotlin.time.measureTime
// Basic usage
val waktu: Duration = measureTime {
Thread.sleep(100) // simulate a time-consuming operation
}
println("Execution time: $waktu") // "Execution time: 100ms" (approximately)
// Benchmarking a function
fun hitungFibonacci(n: Int): Long = when (n) {
0, 1 -> n.toLong()
else -> hitungFibonacci(n - 1) + hitungFibonacci(n - 2)
}
val waktuFib = measureTime {
hitungFibonacci(40)
}
println("Fibonacci(40) takes: $waktuFib")
// Comparing two implementations
val waktuLambat = measureTime {
repeat(1_000_000) { i -> i * i }
}
val hasilCached = (0..999_999).map { it * it }
val waktuCepat = measureTime {
hasilCached.forEach { it }
}
println("Without cache: $waktuLambat")
println("With cache: $waktuCepat")
// A simple multi-run benchmark
fun benchmark(label: String, ulang: Int = 10, blok: () -> Unit): Duration {
// Warmup
repeat(3) { blok() }
val total = measureTime {
repeat(ulang) { blok() }
}
val rataRata = total / ulang
println("[$label] Total: $total | Average: $rataRata per iteration")
return rataRata
}
benchmark("String concat", 100) {
var s = ""
repeat(1000) { s += "x" }
}
benchmark("StringBuilder", 100) {
val sb = StringBuilder()
repeat(1000) { sb.append("x") }
sb.toString()
}
measureTimedValue — Value and Time Together #
measureTimedValue is like measureTime but also returns the value produced by the block.
import kotlin.time.measureTimedValue
// Returns a TimedValue<T> containing the value and the duration
val (hasil, waktu) = measureTimedValue {
hitungFibonacci(35)
}
println("Fibonacci(35) = $hasil, computed in $waktu")
// The explicit type
val timedResult: kotlin.time.TimedValue<List<Int>> = measureTimedValue {
(1..1000).filter { it % 2 == 0 }.map { it * it }
}
println("${timedResult.value.size} elements, took ${timedResult.duration}")
// Useful for performance logging in production
fun <T> withTiming(label: String, threshold: Duration = 100.milliseconds, blok: () -> T): T {
val (nilai, durasi) = measureTimedValue(blok)
if (durasi > threshold) {
println("SLOW [$label]: $durasi (threshold: $threshold)")
} else {
println("OK [$label]: $durasi")
}
return nilai
}
val data = withTiming("fetch data", threshold = 200.milliseconds) {
ambilDataDariDatabase()
}
// A profiling middleware in Ktor
fun Application.configureMonitoring() {
intercept(ApplicationCallPipeline.Monitoring) {
val (_, durasi) = measureTimedValue {
proceed()
}
val path = call.request.path()
if (durasi > 500.milliseconds) {
log.warn("Slow request: $path took $durasi")
}
}
}
Duration in Coroutines #
Duration integrates naturally with Kotlin coroutines, especially for delay and withTimeout.
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.minutes
// delay with Duration — more expressive than delay(milliseconds)
suspend fun prosesAsync() {
// ANTI-PATTERN: delay with a magic number in milliseconds
delay(5000) // 5 seconds? 5000 ms? unclear
// CORRECT: delay with Duration
delay(5.seconds)
delay(30.minutes)
delay(1.5.seconds) // 1500ms
}
// withTimeout with Duration
suspend fun ambilDataDenganTimeout(): String = withTimeout(10.seconds) {
// If this operation takes more than 10 seconds, TimeoutCancellationException is thrown
ambilDataDariApi()
}
// withTimeoutOrNull — returns null on timeout
suspend fun ambilDataAman(): String? = withTimeoutOrNull(5.seconds) {
ambilDataDariApi()
}
// A retry implementation with Duration
suspend fun <T> retryDenganDelay(
maxPercobaan: Int = 3,
jedaAwal: Duration = 1.seconds,
faktorBackoff: Double = 2.0,
blok: suspend () -> T
): T {
var percobaan = 0
var jeda = jedaAwal
while (true) {
try {
return blok()
} catch (e: Exception) {
percobaan++
if (percobaan >= maxPercobaan) throw e
println("Attempt $percobaan failed, waiting $jeda...")
delay(jeda)
jeda = (jeda * faktorBackoff).coerceAtMost(30.seconds)
}
}
}
// Usage
suspend fun main() {
val data = retryDenganDelay(maxPercobaan = 5, jedaAwal = 500.milliseconds) {
ambilDataDariApi()
}
}
Formatting Durations #
val durasi = 1.hours + 23.minutes + 45.seconds + 500.milliseconds
// Built-in toString — short format
println(durasi) // "1h 23m 45.5s"
println(5.seconds) // "5s"
println(1500.milliseconds) // "1.5s"
println(90.minutes) // "1h 30m"
println(0.5.seconds) // "500ms"
println(100.nanoseconds) // "100ns"
// A custom format for user-facing display
fun Duration.formatKustom(): String {
return toComponents { jam, menit, detik, _ ->
buildString {
if (jam > 0) append("${jam}h ")
if (menit > 0) append("${menit}m ")
if (detik > 0 || (jam == 0L && menit == 0)) append("${detik}s")
}.trim()
}
}
println(1.hours.formatKustom()) // "1h"
println((1.hours + 30.minutes).formatKustom()) // "1h 30m"
println((2.hours + 5.minutes + 30.seconds).formatKustom()) // "2h 5m 30s"
// ISO 8601 format
fun Duration.toISO8601(): String {
return toComponents { jam, menit, detik, nanosecond ->
buildString {
append("PT")
if (jam > 0) append("${jam}H")
if (menit > 0) append("${menit}M")
val detikDesimal = detik + nanosecond / 1_000_000_000.0
if (detikDesimal > 0) append("${detikDesimal}S")
}
}
}
// A countdown timer format — HH:MM:SS
fun Duration.formatCountdown(): String {
return toComponents { jam, menit, detik, _ ->
"%02d:%02d:%02d".format(jam, menit, detik)
}
}
println((2.hours + 5.minutes + 7.seconds).formatCountdown()) // "02:05:07"
println(45.minutes.formatCountdown()) // "00:45:00"
// A relative human-readable format
fun Duration.formatRelatif(): String = when {
this < 1.seconds -> "just now"
this < 1.minutes -> "${inWholeSeconds} seconds ago"
this < 1.hours -> "${inWholeMinutes} minutes ago"
this < 1.days -> "${inWholeHours} hours ago"
else -> "${inWholeDays} days ago"
}
println(30.seconds.formatRelatif()) // "30 seconds ago"
println(45.minutes.formatRelatif()) // "45 minutes ago"
println(3.hours.formatRelatif()) // "3 hours ago"
println(2.days.formatRelatif()) // "2 days ago"
Idiomatic Patterns in Production Code #
Cache with TTL (Time-To-Live) #
import kotlin.time.TimeSource
class Cache<K, V>(private val ttl: Duration) {
private val sumber = TimeSource.Monotonic
private val data = mutableMapOf<K, Pair<V, kotlin.time.TimeMark>>()
fun simpan(kunci: K, nilai: V) {
data[kunci] = nilai to sumber.markNow()
}
fun ambil(kunci: K): V? {
val (nilai, waktuSimpan) = data[kunci] ?: return null
return if (waktuSimpan.elapsedNow() < ttl) nilai else {
data.remove(kunci)
null
}
}
fun bersihkan() {
val sekarang = sumber.markNow()
data.entries.removeIf { (_, v) -> v.second.elapsedNow() >= ttl }
}
}
// Usage
val cache = Cache<String, String>(ttl = 5.minutes)
cache.simpan("user:1", "Andi")
val user = cache.ambil("user:1") // "Andi" if not yet 5 minutes
A Simple Rate Limiter #
class RateLimiter(
private val maksPermintaan: Int,
private val jendela: Duration
) {
private val sumber = TimeSource.Monotonic
private val histori = ArrayDeque<kotlin.time.TimeMark>()
@Synchronized
fun izinkan(): Boolean {
val sekarang = sumber.markNow()
// Remove history outside the time window
while (histori.isNotEmpty() && histori.first().elapsedNow() > jendela) {
histori.removeFirst()
}
return if (histori.size < maksPermintaan) {
histori.addLast(sekarang)
true
} else false
}
}
// Maximum 10 requests per minute
val limiter = RateLimiter(maksPermintaan = 10, jendela = 1.minutes)
fun tanganiPermintaan() {
if (!limiter.izinkan()) {
throw TooManyRequestsException("Rate limit reached")
}
// process the request
}
TimeSource — the Monotonic Clock #
import kotlin.time.TimeSource
// TimeSource.Monotonic — more reliable than System.currentTimeMillis for measuring durations
// Not affected by system clock changes or daylight saving time
val sumber = TimeSource.Monotonic
val mulai = sumber.markNow()
// ... do something ...
Thread.sleep(250)
val elapsed: Duration = mulai.elapsedNow()
println("Elapsed: $elapsed") // "Elapsed: 250ms" (approximately)
// Comparing two TimeMarks
val mark1 = sumber.markNow()
Thread.sleep(100)
val mark2 = sumber.markNow()
println(mark1 < mark2) // true — mark1 is earlier
println(mark2 - mark1) // ~100ms — the difference between marks
// Using TimeMark for deadlines
val deadline = sumber.markNow() + 5.seconds
while (deadline.hasNotPassedNow()) {
// Do something within the 5-second limit
Thread.sleep(100)
}
println("Time's up!")
Duration vs Long Milliseconds #
// ANTI-PATTERN: milliseconds as Long — prone to unit bugs
fun buatKoneksi(timeoutMs: Long) { /* ... */ }
fun cacheData(dataTtlMs: Long) { /* ... */ }
// Easy to get wrong:
buatKoneksi(30) // 30 ms? or forgot to multiply by 1000?
buatKoneksi(30_000) // 30 seconds — but not clear from the code
cacheData(5 * 60 * 1000) // 5 minutes — verbose and error-prone
// CORRECT: use Duration
fun buatKoneksi(timeout: Duration) { /* ..., use timeout.inWholeMilliseconds */ }
fun cacheData(ttl: Duration) { /* ..., use ttl.inWholeSeconds */ }
buatKoneksi(30.seconds) // clear: 30 seconds
buatKoneksi(30.minutes) // clear: 30 minutes
cacheData(5.minutes) // clear: 5 minutes
// Interoperability with Java APIs that use Long
fun koneksiBawaan(url: String, timeout: Duration) {
val koneksi = java.net.URL(url).openConnection()
koneksi.connectTimeout = timeout.inWholeMilliseconds.toInt()
koneksi.readTimeout = timeout.inWholeMilliseconds.toInt()
}
Summary #
- Extension properties like
5.seconds,30.minutes,2.hoursare the idiomatic way to create Durations — far clearer than raw millisecond numbers that are prone to unit bugs.inWholeSeconds,inWholeMinutes,inWholeHours(Long) for whole values;inSeconds,inMinutes(Double) to preserve fractions. Use whichever fits the need.toComponents { jam, menit, detik, ns -> }to split a Duration into individual components — useful for formatting and timer displays.measureTime { }measures execution time and returns aDuration.measureTimedValue { }returns aPaircontaining the value and the duration — use both for performance profiling.delay(duration)andwithTimeout(duration)in coroutines accept aDurationdirectly — use these instead ofdelay(milliseconds)which is unclear about its unit.TimeSource.Monotonicis more reliable thanSystem.currentTimeMillis()for measuring durations — unaffected by system clock changes. UsemarkNow()andelapsedNow()for precise measurements.- Duration supports complete mathematical operations: addition, subtraction, multiplication, division, comparison,
coerceAtMost,coerceAtLeast, andabsoluteValue.- Durations can be negative —
30.minutes - 1.hoursproduces-30m. UseisNegative(),isPositive(), andabsoluteValuefor edge case handling.- Parsing from strings:
Duration.parse("PT1H30M")for ISO 8601 format,Duration.parse("1h 30m")for the Kotlin format, andDuration.parseOrNull()for safe handling of invalid input.- Use Duration as a function parameter instead of a
Longmillisecond value — this makes APIs more expressive, safe, and unambiguous about the expected time unit.