hamsa software
← All articles
Automation7 min read

Which nine o’clock does the report follow?

Four London clock examples turn a vague daily schedule into an explicit policy for ordinary, repeated and missing local times.

Two polygons are separated by a zigzag divide, with a circle to one side, suggesting ambiguity between a clock reading and an instant.

Nine is an agreement with a place

“At nine” is often a perfectly adequate agreement between colleagues who share an office clock. The server has not necessarily joined that agreement. A fixed nine UTC job follows London’s nine on some dates and misses it on others. Before configuring the scheduler, decide which promise the report makes: follow local civil time in a named place, or run after a fixed elapsed interval. Both make sense, but the people waiting for the report will notice if the implementation chooses the other one.

Take a daily report intended for 09:00 in Europe/London. In the worked historical examples, 15 January 2024 maps to 09:00 UTC and 15 July maps to 08:00 UTC. A fixed 09:00 UTC job would therefore arrive at 10:00 London time on that July date. Both schedules are coherent; they fulfil different promises. Writing the named zone beside the local time preserves the intention that a single stored offset would lose across the year.

The four mappings below use Python’s zoneinfo with timezone rules version 2026a. For this report, London belongs to the schedule, rather than to whoever opens it. A colleague travelling elsewhere might display delivery in another zone; that need not move the office’s daily job. Keep generating the intended London occurrence and present its resolved instant as needed. Otherwise a personal display preference can quietly become a change to the agreement about when the report should arrive.

One local time can identify two instants

The more revealing example is 01:30 on 27 October 2024 in London. The fixture finds two UTC instants: 00:30 and 01:30 on that date. They are an hour apart, despite sharing the same local date and time. A report scheduled for that wall-clock reading could be intended once, at the earlier occurrence, once at the later occurrence, or at both. The word daily alone does not settle which interpretation is appropriate.

Python represents an ambiguous local time using fold: zero identifies the earlier reading and one the later reading.[1] Zoneinfo applies timezone rules to supply their corresponding offsets.[2] These mechanisms expose the alternatives; the application must choose which occurrence its schedule intends. A constructor's default may select the desired one, but relying on it leaves the intended behaviour undocumented and makes an apparently harmless implementation change harder to assess.

For an overnight summary, a team might choose once per local date and the earlier occurrence of a repeated wall time. Another workflow may need both physical intervals. The person examining an incident log needs to know which rule applies and which UTC instant was selected. Two entries both saying 01:30 are not much help on the night when both are correct. Including the offset or resolved instant preserves the sequence without asking support to supply the missing interpretation from memory.

The report's business period is a separate identity. A run at the later occurrence could still refer to the same local reporting date as a run at the earlier one. If the policy allows both, their occurrence identities must distinguish them; if it allows one, an accidental second trigger must not become another promised daily report merely because it has a different UTC timestamp. Resolving civil time answers when an occurrence is possible. It does not by itself define what business work that occurrence represents.

Some local times do not exist

The fixture also examines 01:30 on 31 March 2024 in London and finds no matching instant. Assigning zone information to that naive datetime does not reliably reject it. PEP 495 explicitly describes constructors and replacement operations that permit missing local times.[1] A programme can therefore hold an object that prints like a plausible zoned time without having established that the civil clock ever displayed that reading. The presence of an offset should not be mistaken for successful validation.

To find the alternatives, try both fold values, convert each candidate to UTC and back, retain matching local fields and deduplicate the resulting instants. Against the recorded timezone data, the checks yield one ordinary instant, two autumn instants and no spring-gap instant. A scheduler must still choose what to do with each outcome. If it silently shifts a missing time while another component rejects it, sharing a plausible datetime object will not make their schedules agree. The round trip exposes the choice so the schedule can name it.

A missing occurrence requires a different policy from an ambiguous one. Skip that date, move to the first valid time afterwards, or preserve the displacement by moving the requested time forward through the gap are distinct choices. For this London example, first valid afterwards and preserving the half-hour displacement would not name the same local reading. A vague setting called shift forward leaves that difference unresolved. Describe the resulting clock time in the requirement so a non-specialist can judge whether the report would still be useful.

A job meant for 09:00 avoids these particular London 01:30 transitions, but that is a property of the chosen zone and dates, not a universal guarantee about morning schedules. The named timezone database supplies rules; the application supplies policy for the outcomes those rules permit. Choosing a convenient time can reduce operational awkwardness, while explicit handling prevents a future configuration change from inheriting an accidental rule that nobody remembers approving.

One local reading can map to zero, one or two instants

Executed calculations for Europe/London using installed timezone data version 2026a.

Scroll sideways to see all columns.

London local readingUTC instant(s)CountDecision required
15 Jan 2024, 09:0015 Jan, 09:00Z1Ordinary occurrence
15 Jul 2024, 09:0015 Jul, 08:00Z1Civil nine differs from UTC nine
27 Oct 2024, 01:3027 Oct, 00:30Z and 01:30Z2Choose earlier, later or both
31 Mar 2024, 01:30None0Choose explicit gap policy
The fixture round-trips candidate instants through UTC; attaching tzinfo alone does not validate a missing local time. Adjacent 09:00 runs span 23 hours in spring and 25 in autumn.

A day is not always twenty-four elapsed hours

The second calculation compares successive London 09:00 occurrences around the same 2024 transitions. From 30 to 31 March, the UTC interval is twenty-three hours. From 26 to 27 October, it is twenty-five. The local appointment remains nine on each date. This is the clearest way to separate a civil schedule from running again after 86,400 elapsed seconds. Neither is a more accurate clock; each is accurate to a different intention.

The report’s contents inherit this distinction. A day defined by local boundaries can have a different elapsed length during a clock change. Derive those boundaries in the relevant zone, then compare the resulting instants consistently, rather than add twenty-four hours to yesterday’s UTC boundary and assume the result is local midnight. Endpoints matter as well: an inclusive end can put the same boundary record in two adjacent reports. A half-open period convention gives that record one home, but remains an application decision to record beside the schedule.

Elapsed intervals remain appropriate for work such as polling a technical dependency every few minutes or delaying a retry. That work need not realign itself to an office clock. An appointment described as every Tuesday at nine usually requires a civil recurrence instead. These are different scheduling objects, so establishing the intended recurrence is necessary before choosing how to store and resolve it. A universal storage rule cannot settle that requirement.

A local daily recurrence need not be 86,400 seconds apart

Europe/London transition examples calculated with the fixture’s timezone data.

Scroll sideways to see all columns.

Local recurrenceElapsed UTC intervalMeaning
09:00, 30 to 31 March 202423 hoursBoth occurrences remain at local nine.
09:00, 26 to 27 October 202425 hoursBoth occurrences remain at local nine.
Repeat after 86,400 seconds24 hoursA fixed elapsed interval expresses another schedule.
The reporting period also needs explicit boundaries; civil-time conversion does not define which records a report includes.

Preserve the intention and the resolution

A useful schedule record keeps the local time, named zone, recurrence and fold/gap policy. A useful occurrence record keeps the intended local date, resolved instant and the policy decision used. Those records serve different purposes: one describes what to generate; the other explains a particular result. The worker should not have to reconstruct last month's intention from the server's current local timezone setting or a log line that says only 01:30.

Timezone data is another dependency. Python documents that zoneinfo obtains rules from system data or the first-party tzdata package, and that environments can differ.[2] Keep deployments supplied with the needed zone data and make its version identifiable when exact reproduction matters. A rule update affecting future schedules needs a decision about which pending occurrences to recalculate. Completed business records should retain their actual resolved instants rather than being silently rewritten to match a new interpretation of the schedule.

Support should be able to explain a report using its intended local date and actual resolved instant. That takes a named zone, a choice for repeated times, a rule for missing times and a definition of the business period. Catching up a missed run is another decision again; converting a clock reading cannot settle it. Once those choices are recorded, a delayed or repeated report becomes something the team can account for, instead of a debate about what everyone thought nine meant when the schedule was first agreed.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. Python PEP 495: local-time disambiguation and invalid times Published 2 August 2015
  2. Python: zoneinfo and timezone-data sources
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
Automation6 min read

A budget that three workers can share

Enforce task limits through atomic reservations and repeat-safe settlement, including the uncomfortable case where a timed-out operation may still be spending.

Automation7 min read

What exactly did that approval authorise?

Bind an assistant’s approval to the action, target and material revision, then enforce that binding when the write happens.