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.

SyntaxDuration 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) is P1Y2M3D, and getDays() is 3, not 427. For a total count use ChronoUnit.DAYS.between(a, b).
  • Calling Duration.between on dates. LocalDate has no seconds, so it throws UnsupportedTemporalTypeException: Unsupported unit: Seconds.
  • Storing durations as bare numbers. A config value of 30 could mean seconds or minutes. Store ISO-8601 text like PT30S and read it with Duration.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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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: PT25H

Timeouts 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.

Example · java
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: 15

Free 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.

Example · java
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

  • Be the first to comment on this lesson.