Every appointment has one true answer to the question "when does this happen", and that answer is an instant in time, not a reading on a clock. Two people in different countries looking at the same booking should see two different local times for the same instant. Getting this right is mostly one decision made early: store the instant, render the zone.
In practice that means the database holds a UTC timestamp, every person record holds an IANA time zone name such as America/Denver or Europe/Berlin, and every screen converts at the moment of display. The booking page shows the invitee slots in the invitee zone. The host dashboard shows the same slots in the host zone. Neither is converted twice, and neither is stored in a local format.
The failure nearly everyone hits at least once is storing a local wall clock time, or a local time stamped with the numeric offset that applied on the day it was booked. Both pass testing perfectly until a daylight saving transition lands between the booking and the appointment, at which point the meeting quietly moves by an hour and nobody finds out until someone is sitting alone on a call. Daylight saving has enough edge cases of its own to get a separate piece: see scheduling across daylight saving.
Store An Instant, Not A Wall Clock Time
A wall clock time is "3pm on 5 November". An instant is "the moment that is 2025-11-05T22:00:00Z". The first is meaningless without a zone. The second is unambiguous everywhere on earth, forever.
A booking record holds the instant. The local times both parties see are derived, computed fresh on every render from the instant plus the viewer zone. Store a rendered local time in a column to avoid converting it again and you have created a value that will eventually disagree with the instant it came from.
There is one important exception, and it is the source of most of the confusion in this area. A recurring appointment is not a set of instants, it is a rule: "every Tuesday at 10:00 in America/Denver". That rule has to be stored as a local time plus a zone name, because the intent is that it stays at 10:00 for the host across a daylight saving change. The instants are then generated from the rule. Storing a recurring series as a frozen list of UTC instants is exactly how a weekly meeting ends up at 09:00 for half the year. There is more on that in setting up recurring appointments.
| What You Store | Example | Survives A DST Change | Survives A Zone Rule Change | Right For |
|---|---|---|---|---|
| Naked local time | 2025-11-05 15:00 | No | No | Nothing |
| Local time plus fixed offset | 2025-11-05 15:00 -06:00 | No, if captured before the change | No | Logging an instant that already happened |
| UTC instant | 2025-11-05T21:00:00Z | Yes | Yes | A one off booking |
| Local time plus IANA zone name | 2025-11-05 15:00 America/Denver | Yes | Yes, once the zone database is updated | Recurrence rules and availability rules |
Why IANA Names Beat Fixed Offsets
An offset such as -06:00 or +02:00 tells you the difference from UTC at one moment. It tells you nothing about what that difference will be next month. Denver is at -06:00 in July and -07:00 in December. If you record "the user is at -06:00" in July, every winter appointment you compute for them is an hour out.
An IANA zone name such as America/Denver is not an offset, it is a pointer into a maintained database of rules: when that region changes its clocks, by how much, and what it did historically. Ask it for the offset at a specific instant and you get the right answer, including for future dates under currently published rules. That is why offsets are wrong for future dates specifically. For a past timestamp an offset is a fine record of what happened, but for anything scheduled ahead only a zone name carries enough information.
Two practical consequences follow. Collect the zone name, not the offset, since browsers expose it directly. And keep the zone database current, because governments change daylight saving rules at short notice and a stale database will confidently compute the wrong local time for the countries that changed.
Getting The Invitee Zone Right
There are three ways a booking page can decide which zone to show, and they should be used in this order:
- Detect it from the browser, which reports the operating system zone. This is right the large majority of the time.
- Let the invitee change it with a visible selector on the booking page. Someone travelling, or booking on behalf of a colleague, needs this. Hiding the selector to keep the page clean is a false economy.
- Remember it for a returning invitee, but let the detected value override a stale stored one, and say which zone you are using rather than assuming.
The label matters too. "10:00 (Europe/Berlin)" is unambiguous. "10:00" is a support ticket waiting to happen.
Show The Invitee Their Zone, And The Host Theirs
Availability is defined in the host zone, because that is where the working day exists. A host who works 09:00 to 17:00 means 09:00 to 17:00 where they are sitting. The system converts those boundaries into instants, then renders the resulting slots in whatever zone the person looking at the page is in.
The consequence is that the same slots look completely different to different invitees, and that is correct. Nobody has to do arithmetic, which is the entire point.
Confirmations, reminders and calendar invitations need the same treatment. The invitee copy shows invitee local time, the host copy shows host local time, and the ICS attachment carries the instant so the receiving calendar renders it itself. If reminders render in server time, every overseas client gets one that appears to be for the wrong appointment.
A Worked Example: Denver And Berlin
A consultant works in America/Denver, 09:00 to 17:00, Monday to Friday, one hour sessions. A client is in Europe/Berlin. Here is what each of them sees for the same underlying availability at three points in the year.
| Date | Denver Offset | Berlin Offset | Gap | Host Sees 10:00 | Client Sees |
|---|---|---|---|---|---|
| Mid July | -06:00 | +02:00 | 8 hours | 10:00 | 18:00 |
| Late October, after Europe changes but before the US does | -06:00 | +01:00 | 7 hours | 10:00 | 17:00 |
| Mid December | -07:00 | +01:00 | 8 hours | 10:00 | 18:00 |
Walk one booking through it. The client picks the slot labelled 18:00 in July. The system computes the instant, 10:00 in Denver at -06:00 being 16:00 UTC, and stores 2025-07-15T16:00:00Z. The host dashboard renders it in Denver as 10:00, the confirmation renders it in Berlin as 18:00. One stored value, two correct renderings.
Now the middle row. For roughly a week each autumn, Europe has moved its clocks back and North America has not. The gap narrows to seven hours, so the host still sees 10:00 and the client sees 17:00. That is not a bug, it is what the two countries have chosen, and a system storing instants and IANA names produces it automatically.
The Classic Failure: A Booking That Drifts
Here is the failure in its usual shape. In October, a client books a session for 5 November at 15:00 Denver time. The system stores the local wall clock string 2025-11-05 15:00 together with the offset in force on the day of booking, -06:00, which resolves to 2025-11-05T21:00:00Z.
On 2 November, the United States moves to standard time and Denver becomes -07:00. The stored instant has not changed, so on 5 November the system renders 21:00Z in Denver as 14:00. The client turns up at 15:00 as agreed and the host has it in the diary for 14:00. Everything is internally consistent, and the appointment is an hour wrong.
The fix is to resolve the local time using the zone name and the target date, not the offset in force today. Ask the zone database for the UTC instant of 15:00 on 5 November in America/Denver and it answers 22:00Z, because it knows the region will be on standard time by then. The same bug in a different costume is an appointment that drifts because a calendar integration re-imported it with the wrong zone assumption, covered in why calendar sync fails.
Where Zones Leak Into The Rest Of The System
Time zone handling is rarely confined to the booking screen. The places it commonly leaks:
- Reminder scheduling. "24 hours before" is arithmetic on instants and is safe. "The evening before" is a local concept and needs the recipient zone.
- Availability rules and cut-offs. Working hours, breaks, overrides and rules such as "no bookings after 5pm for tomorrow" are local wall clock concepts evaluated in the host zone, never the server zone. See setting your availability rules.
- Reports and exports. "Bookings today" means different rows depending on whose today you mean, so pick one and label it, and emit ISO 8601 with an explicit offset rather than a bare local timestamp.
How To Test Time Zone Handling
Most of these bugs are invisible to someone testing in the same zone as the server, in the same month the code was written.
- Set the server to UTC and leave it there. A server in a zone with daylight saving turns every scheduled job into a puzzle.
- Test a host and an invitee in different zones, including a pair where the date differs, such as Los Angeles and Sydney. Confirm the day label as well as the hour.
- Test across each transition, and in a southern hemisphere zone such as
Australia/Sydneywhere the change runs the opposite way. - Test a non whole hour offset such as
Asia/Kolkataat +05:30, which catches code assuming offsets are whole hours, and test the ambiguous and non existent hours at each transition. - Check the outbound artefacts, not only the screen: the confirmation email, the reminder, the ICS attachment and the API payload.
How This Works In appntmnts.io
appntmnts.io stores bookings as instants and holds an IANA zone name against each host and each invitee. Booking pages detect the visitor zone and show a selector so it can be corrected, availability is defined in the host working zone, and confirmations, reminders and calendar invitations render in the recipient zone rather than a server default.
If you are moving off a spreadsheet or a hand managed diary and cross border bookings are part of the job, the features page covers how availability, sync and reminders fit together, and pricing shows what is included on the free plan. The next thing worth reading is scheduling across daylight saving, which handles the transition weeks in detail.