Time Zones as a Data Problem: tzdata Updates, java.time, and the Bugs That Survive
Most developers treat time zones as arithmetic. In reality, they are laws, and laws change on short notice. That makes your JDK a snapshot of legislation, and snapshots go stale.
Why time zones are a data problem
A time zone is not a formula. It is a list of political decisions, such as when a region changes its clocks or abandons daylight saving time. Consequently, the rules live in a database, the IANA tz database, and volunteers publish new releases whenever a government changes its mind.
2026 has been a busy year. Moreover, the changes are not just future ones, because one release even rewrote the past. Table 1 summarizes the five releases so far, based on the official tz release notes.
| Release | Date | What changed |
|---|---|---|
| 2026a | Mar 1 | Moldova has used EU transition times since 2022 (a change to past timestamps) |
| 2026b | Apr 22 | British Columbia moved to permanent -07 |
| 2026c | Jul 8 | Alberta moved to permanent -06; Morocco plans permanent UTC from Sep 20 |
| 2026d | Sep 11 | Northwest Territories moved to permanent -06 |
| 2026e | Sep 29 | Manitoba moves to permanent -05 on Oct 31 |
Table 1. The 2026 tzdata releases and their headline changes.
How often does the data move?
Counting the release headings in the same notes gives a clear picture. Some years are quiet, whereas others, like 2014 and 2016, saw ten releases each. Figure 1 shows the trend since 2014.

What java.time does, and what it cannot do
The java.time API is excellent at applying rules. It handles offsets, gaps, and overlaps correctly. However, it can only apply the rules that your runtime knows about. Because those rules ship inside the JDK, an old JDK simply believes in an old world.
Check which tzdata your JVM runs
You can ask the JVM directly. The snippet below prints the tzdata version and the offset of five zones on 1 December 2026. It needs JDK 11 or newer, because it uses the single-file source launcher. I ran it on the JDK 21 build in my test environment.
cat > TzCheck.java <<'EOF'
import java.time.*;
import java.time.zone.ZoneRulesProvider;
public class TzCheck {
public static void main(String[] args) {
System.out.println("JDK " + System.getProperty("java.version"));
System.out.println("tzdata "
+ ZoneRulesProvider.getVersions("Europe/Athens").lastKey());
LocalDateTime t = LocalDateTime.of(2026, 12, 1, 12, 0);
String[] ids = {"America/Vancouver", "America/Edmonton",
"America/Winnipeg", "Africa/Casablanca", "Europe/Athens"};
for (String id : ids) {
System.out.println(id + " -> "
+ ZonedDateTime.of(t, ZoneId.of(id)).getOffset());
}
}
}
EOF
java TzCheck.java
My JDK reported tzdata 2026b. To know the correct answers, I compiled tzdata 2026e from the official source and queried the same zones. Table 2 compares the two.
| Zone | JDK 21.0.12.1 (tzdata 2026b) | tzdata 2026e | Verdict |
|---|---|---|---|
| America/Vancouver | -07:00 | -07:00 | Correct |
| America/Edmonton | -07:00 | -06:00 | One hour off |
| America/Winnipeg | -06:00 | -05:00 | One hour off |
| Africa/Casablanca | +01:00 | +00:00 | One hour off |
| Europe/Athens | +02:00 | +02:00 | Correct |
Table 2. Offsets on 2026-12-01 at 12:00 local time. The JDK is an Ubuntu OpenJDK build; other vendors’ builds may carry other tzdata versions.
Notice the pattern. The zones changed by releases after 2026b are wrong, and the rest are fine. As a result, a service running on this JDK would convert an Edmonton or Winnipeg meeting to the wrong instant, silently and without any exception.
The bugs that survive
Stale rules in long-lived runtimes
Containers and base images make this worse, since they freeze a JDK for months. Fortunately, the fix is routine. Oracle recommends the latest JDK update release as the main way to receive timezone data, and it offers the TZUpdater tool for runtimes that cannot upgrade yet.
Timing matters too. Figure 2 shows how many days passed between each 2026 release and the first clock change it affects. The window has been shrinking, and Manitoba’s change left only 33 days.

Gaps, overlaps, and days that are not 24 hours
Even with perfect data, local times can be ambiguous. Athens falls back this Sunday, 25 October 2026, so the next snippet is timely. According to the ZonedDateTime documentation, a gap shifts the time forward, while an overlap keeps the earlier offset by default.
cat > DstEdges.java <<'EOF'
import java.time.*;
public class DstEdges {
public static void main(String[] args) {
ZoneId athens = ZoneId.of("Europe/Athens");
// Gap: 03:00 jumps to 04:00 on 2026-03-29
LocalDateTime gap = LocalDateTime.of(2026, 3, 29, 3, 30);
System.out.println("gap " + ZonedDateTime.of(gap, athens));
// Overlap: 03:30 happens twice on 2026-10-25
LocalDateTime twice = LocalDateTime.of(2026, 10, 25, 3, 30);
ZonedDateTime first = ZonedDateTime.of(twice, athens);
System.out.println("first " + first);
System.out.println("second " + first.withLaterOffsetAtOverlap());
// "Plus one day" is not always 24 hours
ZonedDateTime day =
LocalDate.of(2026, 10, 25).atStartOfDay(athens);
System.out.println("hours "
+ Duration.between(day, day.plusDays(1)).toHours());
}
}
EOF
java DstEdges.java
The output shows 03:30 resolved to 04:30 in the gap, two different instants for the overlapping 03:30, and a 25-hour day. Therefore, never assume that “tomorrow at the same time” is exactly 24 hours away.
What to store: instants or local times
The popular advice is to store everything in UTC. However, Jon Skeet’s well-known post, Storing UTC is not a silver bullet, explains why that fails for future events. The rule of thumb is simple. For things that already happened, store an Instant. For future events that people schedule in local time, store the local date-time plus the zone ID.
The reason is the Winnipeg row in Table 2. A meeting at noon on 1 December, converted to UTC under the old rules, would be an hour off after Manitoba’s change. Storing the local time and zone lets the new rules fix it automatically, as soon as the runtime updates.
Time zone checklist
- Log the tzdata version at startup, using
ZoneRulesProvider. - Rebuild images and upgrade the JDK regularly, not only for security fixes.
- Store an
Instantfor past events, and local time plus zone ID for future ones. - Test gaps, overlaps, and 25-hour days around every DST change.
- Use zone IDs such as
Europe/Athens, never fixed offsets, for people’s schedules.
What we have learned
Time zones are legislation stored as data, and the tz database changes several times a year, with five releases already in 2026 covering Moldova, British Columbia, Alberta, Morocco, the Northwest Territories, and Manitoba. The java.time API applies rules correctly, but the rules ship inside your JDK, so an old runtime quietly returns wrong offsets. In our test, a JDK carrying tzdata 2026b was one hour off for three of five zones on 1 December 2026. Because the lead time before a clock change shrank to as little as 33 days, regular JDK updates, or Oracle’s TZUpdater, are essential. Beyond that, gaps and overlaps still need explicit handling, and future local events should be stored as local time plus zone ID rather than as UTC alone.

