Time and Time Zones: Four Falsehoods and How to Survive Them

By Sergey Nosov

30 September 2026

At 2:00 a.m. on Sunday, 1 November 2026, clocks across most of the United States will fall back to 1:00 a.m. For the next hour, every local time will happen twice. A nightly job scheduled for 1:30 a.m. may run twice, and a report that groups rows by local hour will count one hour of data as two. Canada will be less uniform than usual. British Columbia, Alberta, the Northwest Territories, and Manitoba all decided this year to stop falling back, and Manitoba announced its decision only on 17 September. A phone or server whose time zone data predates those decisions will still fall back for them and show local time there an hour behind the clocks on the wall.

Time looks like the simplest data a program handles: a number that goes up. Almost everything else we intuitively believe about it is false. In June 2012, Noah Sussman collected the mistakes he kept finding in other engineers’ code. He called the post “Falsehoods Programmers Believe about Time”, and a follow-up soon added more. A developer who publishes as timvisee later merged both into a single list. It opens with a warning worth taking literally: “If you think you understand everything about time, you’re probably doing it wrong.”

This article takes on the four falsehoods I consider the most expensive: that every day has 24 hours, that offsets are whole hours, that the rules hold still, and that time always moves forward. Then it turns to the habits that keep code correct anyway, a worked example with three quiet bugs, and a list of dates worth testing.

Falsehood 1: Every Day Has 24 Hours

Twice a year, in every zone that observes daylight saving time, a day breaks this rule. In the United States and Canada, clocks spring forward at 2:00 a.m. on the second Sunday in March and fall back at 2:00 a.m. on the first Sunday in November. On 8 March 2026, the day in New York lasted 23 hours, and 2:30 a.m. never happened. Depending on the scheduler, a job set for that minute ran late or not at all. On 1 November 2026, the day will last 25 hours, and 1:30 a.m. will happen twice. Depending on the scheduler, the same job may run once or twice.

Some days do not even have a midnight. Until Brazil abolished daylight saving time in 2019, its clocks sprang forward at midnight, jumping from 23:59:59 straight to 01:00. Code that computed “the start of the day” as 00:00 produced a time that did not exist. In data pipelines, the symptoms of this falsehood are duplicated or missing rows and daily totals that are an hour too wide or an hour too short. They arrive on schedule, twice a year.

Even the minute is not safe. Since 1972, UTC has added twenty-seven leap seconds to keep in step with the Earth’s rotation, each one a minute that ends at 23:59:60. The leap second at the end of 30 June 2012 triggered a bug in the Linux kernel. On 2 July 2012, The Register reported that the Amadeus Altea reservation system “was taken offline for an hour.” Staff at Qantas and Virgin Australia checked in passengers by hand. Servers at Mozilla, Reddit, LinkedIn, and others were reported hit by the same bug.

Google’s answer was to stop showing its machines the extra second. According to its leap smear page, Google has “smeared” each leap second across the hours around it since 2008. Its clocks run slightly slow for a while, so no server ever reads :60. The page recommends “a 24-hour linear smear from noon to noon UTC” and says that “Amazon uses this smear in AWS.” The leap second itself may be on its way out. In 2022, the General Conference on Weights and Measures decided that the maximum difference between UTC and the Earth’s rotation “will be increased in, or before, 2035.” The tz database maintainers note that “possibly there will be no more, due to planned changes to UTC.” There has been none since the end of 2016, and the IERS has announced none for the end of December 2026. Old data still contains 23:59:60, though, and RFC 3339 allows it.

The test worth remembering: if your code equates “tomorrow” with “now plus 24 hours,” it is wrong twice a year, somewhere, for someone. Before fixing it, decide what the business actually means. “Twenty-four hours after checkout” is elapsed time and belongs on the UTC timeline. “Same time tomorrow” is calendar time and belongs in the local zone. Across a fall-back night, the two differ by an hour, and each is the right answer to a different question.

Falsehood 2: Offsets Are Whole Hours

Nepal runs on UTC+05:45. India, which the United Nations projected would become the world’s most populous country in April 2023, runs on +05:30. Iran uses +03:30, and New Zealand’s Chatham Islands use +12:45 in winter and +13:45 in summer. A schema with an integer “offset hours” column cannot store the local time of the most populous country on Earth. Neither can a validation rule that accepts only whole hours or a drop-down list with twenty-four entries.

The range is wider than most people expect, too. Inhabited places run from UTC−11:00 in American Samoa to UTC+14:00 on Kiritimati in Kiribati. For an hour every day, from 10:00 to 11:00 UTC, three calendar dates are in effect on Earth at once. “What day is it?” has no single global answer.

Daylight saving time is not always an hour either, and the falsehood list says so: “Daylight saving time always adjusts by an hour.” Lord Howe Island, off the coast of Australia, moves its clocks by 30 minutes, from +10:30 to +11:00. The tz database models the Troll research station in Antarctica as jumping 2 hours at once, between UTC and +02:00, on the European Union’s dates. Offsets are data, not arithmetic.

Falsehood 3: The Rules Hold Still

Whatever the rules are today, governments change them, often on short notice. A few examples from the record:

All of these rules live in one place: the IANA time zone database, often called tzdata. Its documentation describes it as the product of “collaboration among governments, the time zone database volunteer community, and data distributors downstream.” There is no fixed schedule; the maintainers say that “typically a release occurs every few months.” So far in 2026 there have been five, the latest on 29 September. The project is also candid about its limits: “The tz database is not authoritative, and it surely has errors.”

Corrections reach into the past as well. The two September 2026 releases also changed the date on which Ireland fell back in 1925 and moved Colombia’s 1992 and Iran’s 1979 spring-forward transitions a full day earlier. A tzdata update can change what a local timestamp stored decades ago means in UTC.

Every runtime carries its own copy, and each copy updates on its own schedule. Linux distributions ship tzdata as a package. The JDK has its own, which Oracle’s TZUpdater tool can refresh. Python’s zoneinfo reads the system copy and falls back to a tzdata package on PyPI. Chrome and Node.js get theirs through ICU, whose project says it “derives its tz data from the IANA Time Zone Database.” The Noda Time library for .NET embeds one. On Windows, .NET accepts IANA names but takes the rules from Windows’ own time zone data in the registry. The tz project spells out what has to happen after each release: “After a release, various parties must integrate, test, and roll out an update before end users see changes.”

When those copies disagree, people notice. On 4 September 2026, CBC News reported on a Calgary hairstylist whose online booking system had moved his appointments. The report said that “all of the appointments he had prebooked from November to March had also been pushed back an hour,” and his salon books eight months ahead. In his words: “we’re not having a time change, but [the computer] thinks we are.” The report does not say where the booking system went wrong. An exact one-hour shift, confined to the months that used to be standard time, has a familiar cause. It is what you get when an appointment was converted to UTC under one version of the rules and is displayed under another.

So treat tzdata like a security dependency. Know which version each runtime carries, and patch it on the same cadence as everything else. Node.js, for example, reports its version in process.versions.tz. On the machine where I checked this article’s samples, Node.js 22.22.0 still reported tz 2025b and put Edmonton at UTC−07:00 in December 2026. Current rules say UTC−06:00.

Falsehood 4: Time Always Goes Forward

The falsehood list puts it plainly: “Time always goes forwards.” It does not. The system clock can jump when NTP steps it to correct an error, when a virtual machine resumes from a pause, or when someone changes it by hand. The jump can go backward. Local time steps back an hour every fall. The next timestamp you read can be earlier than the last one.

Client clocks are worse. The list includes the belief that server and client clocks “would never be different by a matter of decades.” Never trust a timestamp the client computed, and validate expiring tokens with a little tolerance. The JSON Web Token standard, RFC 7519, allows for it explicitly. “Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew.”

The symptoms are familiar: negative durations in logs, timeouts that fire at once or never, cache entries that outlive their time to live, and the classic “token not valid yet” error. The cure is to use the right clock for each job. A wall clock tells you what time it is. A monotonic clock tells you how long something took, and it never runs backward:

Subtracting two wall-clock readings does not give you a duration. Never swap the two jobs.

Store the Rulebook, Not the Number

Four habits follow from the four falsehoods.

An offset is not a time zone. UTC−05:00 is a fact about one instant. America/New_York is a rulebook that covers the past and the future, including every date on which the offset changes. Persist the rulebook’s name, never the number. Check what your database really keeps. PostgreSQL’s timestamp with time zone, for example, does not store a zone at all. Its documentation says the value “is stored internally as UTC, and the originally stated or assumed time zone is not retained.” If you need the zone, store it in its own column.

Past events get the UTC instant. An event that already happened occupies one point on the shared timeline, and that fact never changes. A bare local timestamp cannot say the same, because every fall one hour of local time is ambiguous. The falsehood list covers this too: “A given date and/or time unambiguously identifies a unique moment.” Jon Skeet makes the same distinction in his 2019 post “Storing UTC Is Not a Silver Bullet”. Of machine-generated timestamps, he writes: “Storing those in UTC is entirely reasonable.”

Future events get local wall time plus the IANA zone. Derive UTC as late as possible, and recompute it when the rules change. A 9:00 a.m. appointment must stay at 9:00 a.m. even if the government changes the rules between booking and arrival, and as the previous sections show, it might. The tz project’s own documentation gives the warning with an example. Someone schedules a meeting for 13:00 on a future date, Dublin time, and Ireland then changes its rules. In that case, “software can mess up after the rule change if it blithely relies on conversions made before the change.” Skeet calls the general advice to convert everything to UTC and store that “overly broad.” His reason is that for future events “it doesn’t take into account that time zone rules change.”

For a hypothetical appointment in Calgary, that means storing two fields:

starts_local  2026-12-15 09:00
time_zone     America/Edmonton

Under the tz rules published before July 2026, that appointment was at 16:00 UTC. Under current rules it is at 15:00 UTC. The customer’s appointment did not move; only the derived instant did. If you must exchange such a value as one string, RFC 9557, published in April 2024, extends the familiar RFC 3339 format with a bracketed zone name: 2026-12-15T09:00:00-06:00[America/Edmonton]. The format matches the one Java’s ZonedDateTime already printed, and JavaScript’s Temporal uses it too.

Future local times need one more decision: what to do when the stored time falls into a gap or an overlap. Temporal makes the choice explicit with a disambiguation option: "compatible" (the default), "earlier", "later", or "reject". In .NET, TimeZoneInfo offers IsInvalidTime and IsAmbiguousTime. On .NET 10, ConvertTimeToUtc throws for a skipped time and treats a repeated time as standard time. Pick the policy on purpose, not by accident.

Let a library do the math. Use Noda Time or TimeZoneInfo in .NET, java.time on the JVM, zoneinfo in Python, and Temporal in JavaScript. Temporal reached Stage 4 at TC39 in March 2026 and shipped in Firefox 139, Chrome 144, and Node.js 26. Safari had not shipped it by September 2026, so a polyfill or Luxon still has a place. The moment you type an offset by hand, something has gone wrong. The falsehood list has an entry for that, too: “I can easily maintain a timezone list myself.”

The summary rule: machines talk UTC to each other, and people get their own zone at the edge, at the last possible moment.

Worked Example: Three Quiet Bugs

Here are three lines that many codebases contain in some form. Each one passes its tests on most days of the year.

// "Cutoff is 24 hours after
// checkout," on local wall time:
// 23 or 25 real hours, twice
// a year.
var cutoff = order.CreatedLocal
    .AddHours(24);

// "Eastern time is UTC-5":
// wrong all summer.
var nyTime = utcNow.AddHours(-5);

// A hand-kept table: India is
// +5:30, not +5, and each entry
// expires at a rule change.
var offsetHours =
    new Dictionary<string, int>
    {
        ["New_York"] = -5,
        ["India"] = 5,
    };

The first bug hides in the arithmetic. Adding 24 hours to a local time gives the same clock time on the next day. In New York, that is 25 real hours after 6:00 p.m. on 31 October 2026 and 23 real hours after 6:00 p.m. on 7 March 2026. The second line is right from November to March and wrong for the rest of the year. The third is not even waiting for a rule change. It is wrong on arrival, because India is at +05:30.

The cure keeps durations on the UTC timeline and asks the rulebook for wall time:

// Elapsed time: arithmetic on
// UTC instants.
var cutoffUtc = order.CreatedUtc
    .AddHours(24);

// Wall time: ask the zone's
// rulebook by name.
var ny = TimeZoneInfo
    .FindSystemTimeZoneById(
        "America/New_York");
var nyTime = TimeZoneInfo
    .ConvertTimeFromUtc(
        utcNow, ny);

One IANA name replaces the whole table, and the library carries every rule, daylight saving time included. I checked both versions on .NET 10, where FindSystemTimeZoneById accepts IANA names on Windows as well as on Linux and macOS. If the business rule really is “same time tomorrow,” do calendar arithmetic in the zone instead. Temporal makes the difference visible:

const created =
  Temporal.ZonedDateTime.from({
    timeZone: "America/New_York",
    year: 2026, month: 10,
    day: 31, hour: 18,
  });

created.add({ hours: 24 })
  .toPlainDateTime().toString();
// "2026-11-01T17:00:00"

created.add({ days: 1 })
  .toPlainDateTime().toString();
// "2026-11-01T18:00:00"

Twenty-four hours after 6:00 p.m. on 31 October is 5:00 p.m. on 1 November. One calendar day later is 6:00 p.m., 25 hours on. I checked this sample with the @js-temporal/polyfill package. Neither answer is wrong. They answer different questions, and the code should say which one it is asking.

Test the Cursed Days

Time bugs hide because tests run on ordinary days. Make the clock a dependency you can set. In .NET 8 and later, that is TimeProvider, with FakeTimeProvider for tests. In Java, it is java.time.Clock, whose documentation says its “primary purpose” is “to allow alternate clocks to be plugged in as and when required.” Then run the tests on the days that break code:

Takeaways

One exercise to finish: find one place where your code touches time, such as a scheduled job, a report boundary, or a date picker. Ask it what happens on 1 November. If you do not like the answer, you have about a month to fix it.

Further Reading