Duration and Period
Measure amounts of time with Duration for exact seconds and Period for calendar days, months and years, and avoid mixing them up.
Duration d = Duration.between(startInstant, endInstant);
Duration timeout = Duration.ofSeconds(5); // or Duration.parse("PT5S")
Period trial = Period.ofDays(14); // or Period.parse("P14D")
long days = ChronoUnit.DAYS.between(from, to);Java has two types for "an amount of time", because there are two different ideas. Duration is an exact length in seconds and nanoseconds: 30 seconds, 90 minutes, 24 hours. Period is a number of calendar years, months and days: one month, two years, 14 days. One month is not a fixed number of seconds, so it cannot be a Duration.
Duration: time on the clock
Use Duration for timeouts, cache lifetimes, retry delays, token lifetimes and anything you measure between two Instant values. Duration.between(start, end) works on instants and times. toMillis() and toMinutes() give totals; toHoursPart() and toMinutesPart() give the pieces for display.
Duration.ofDays(1) is exactly 24 hours. Added to a ZonedDateTime on the day the clocks change, it does not land at the same wall-clock time.
Period: time on the calendar
Use Period for free trials, billing cycles, notice periods and ages. Period.between(birth, today) returns something like P1Y2M3D. Added to a date, it moves by calendar units, so a one-month trial starting on 31 January ends on 28 February.
Mistakes people make
- Reading
getDays()as a total.Period.between(2025-01-15, 2026-03-18)isP1Y2M3D, andgetDays()is 3, not 427. For a total count useChronoUnit.DAYS.between(a, b). - Calling
Duration.betweenon dates.LocalDatehas no seconds, so it throwsUnsupportedTemporalTypeException: Unsupported unit: Seconds. - Storing durations as bare numbers. A config value of
30could mean seconds or minutes. Store ISO-8601 text likePT30Sand read it withDuration.parse, or put the unit in the name. - Measuring elapsed time with wall-clock instants. The system clock can be adjusted while your code runs. For timing code inside one JVM,
System.nanoTime()is monotonic; use instants for times that other systems see.
ISO-8601 text
Both types print and parse ISO-8601 durations: PT1H30M for a Duration, P1Y2M or P14D for a Period. These strings are a good fit for configuration files, environment variables and JSON fields, because the unit travels with the value.
Duration timeout = Duration.parse(System.getProperty("timeout", "PT5S"));Example
When to use it
- Read HTTP client timeouts and cache TTLs from configuration as ISO-8601 strings like PT5S with Duration.parse.
- Compute the end of a 14-day free trial or a one-month billing cycle with Period so month lengths are respected.
- Report how long a background job took as a Duration between two Instants in the job's status endpoint.
More examples
One day is not always 24 hours
The clocks go back overnight, so the calendar day is 25 hours long. A daily reminder should use Period and stay at 09:00; a 24-hour token lifetime should use Duration.
void main() {
ZonedDateTime before = ZonedDateTime.of(
LocalDateTime.of(2026, 10, 24, 9, 0), ZoneId.of("Europe/London"));
ZonedDateTime calendar = before.plus(Period.ofDays(1));
ZonedDateTime exact = before.plus(Duration.ofDays(1));
IO.println("Period.ofDays(1): " + calendar);
IO.println("Duration.ofDays(1): " + exact);
IO.println("real length: " + Duration.between(before, calendar));
}
// Output:
// Period.ofDays(1): 2026-10-25T09:00Z[Europe/London]
// Duration.ofDays(1): 2026-10-25T08:00Z[Europe/London]
// real length: PT25HTimeouts and TTLs from configuration
An ISO-8601 string carries its own unit, so nobody has to guess whether 15 meant seconds or minutes. Rejecting a bare number at start-up is better than a cache that silently expires every 15 milliseconds.
Duration setting(Map<String, String> config, String key, Duration fallback) {
String raw = config.get(key);
if (raw == null) {
return fallback;
}
try {
return Duration.parse(raw);
} catch (DateTimeException e) {
throw new IllegalArgumentException(key + " is not an ISO-8601 duration: " + raw);
}
}
void main() {
Map<String, String> config = Map.of("http.timeout", "PT2.5S", "cache.ttl", "PT15M");
IO.println(setting(config, "http.timeout", Duration.ofSeconds(5)).toMillis() + " ms");
IO.println(setting(config, "cache.ttl", Duration.ofMinutes(5)).toSeconds() + " s");
IO.println(setting(config, "retry.delay", Duration.ofMillis(200)));
try {
setting(Map.of("cache.ttl", "15"), "cache.ttl", Duration.ZERO);
} catch (IllegalArgumentException e) {
IO.println(e.getMessage());
}
}
// Output:
// 2500 ms
// 900 s
// PT0.2S
// cache.ttl is not an ISO-8601 duration: 15Free trials and ages
Period.between handles the 'has the birthday happened yet this year' check for you, so getYears() is a correct age. Days left uses ChronoUnit.DAYS.between because it needs a total, not a component.
record Trial(LocalDate start, Period length) {
LocalDate end() { return start.plus(length); }
long daysLeft(LocalDate today) {
return Math.max(0, ChronoUnit.DAYS.between(today, end()));
}
}
int age(LocalDate birth, LocalDate today) {
return Period.between(birth, today).getYears();
}
void main() {
Trial t = new Trial(LocalDate.of(2026, 10, 1), Period.ofDays(14));
LocalDate today = LocalDate.of(2026, 10, 13);
IO.println("trial ends " + t.end() + ", days left " + t.daysLeft(today));
IO.println("age " + age(LocalDate.of(2008, 10, 14), today));
IO.println("age " + age(LocalDate.of(2008, 10, 13), today));
}
// Output:
// trial ends 2026-10-15, days left 2
// age 17
// age 18
Discussion