At 9:00 a.m. Eastern, the keynote starts. A third of your virtual audience sees 6:00 a.m. on the schedule card, 6:00 a.m. on the calendar invite, and 6:00 a.m. on the countdown timer. They show up at 6:00. The keynote is over. The recording is fine. The trust is not.
This is not a rendering bug. It is a data-model bug that surfaces as a rendering bug. The schedule card, the calendar invite, the countdown, and the email reminder each resolved the same event through a different code path, and at least one of those paths treated a wall-clock time as if it were an instant. The fix is not a better formatter. The fix is deciding, once, which fields are instants and which are wall times, and then refusing to let any display layer guess.
Two kinds of time, one schedule
A conference schedule contains both. The keynote start is an instant: a single moment that every attendee experiences simultaneously, regardless of where they are. The “doors open” line on the venue signage is a wall time: 8:30 a.m. in the room’s local zone, meaningful only to people standing in that room. A virtual track’s “lunch break” is usually a wall time in the event’s anchor zone, because the speakers and the production crew are working that day, not the attendees’ days.
The W3C’s Working with Time and Timezones draft note draws the distinction directly: an instant is an incremental time on the timeline, while a wall time is a field-based value that is not fixed to a specific moment. The same note names the failure mode you are trying to avoid — a “ghost time,” a wall time that can never exist because of time zone or calendar rules. In America/Los_Angeles, 2:34 a.m. on 2024-03-10 is a ghost time: the clock skips from 1:59 a.m. to 3:00 a.m.
If your schedule model stores only one field, you have already chosen. If it stores a local time and a zone name, you have chosen wall time. If it stores an offset and a zone name, you have chosen something ambiguous, because the offset can be stale while the zone name is current.
Why the offset is not the zone
The most common shortcut in event tooling is to store a timestamp with a UTC offset and call it done. 2026-10-14T09:00:00-04:00 is a valid RFC 3339 timestamp. It is also a claim that the offset for that location on that date is -04:00. That claim is true until a political body changes it.
RFC 9557, the Internet Extended Date/Time Format, was published in April 2024 specifically to attach a time zone name to a timestamp. Its abstract states that it “defines an extension to the timestamp format defined in RFC 3339 for representing additional information, including a time zone.” The format looks like this:
2026-10-14T09:00:00-04:00[America/New_York]
The bracketed suffix is the IANA time zone identifier. The offset is still present, but the zone name is what carries the intent. RFC 9557 is explicit about why this matters: “The use of a named IANA Time Zone implies that the intent is for the rules that are current at the time of interpretation to apply: the additional information conveyed by using that time zone name is to change with any rule changes as recorded in the IANA Time Zone Database.”
The same document warns against the opposite shortcut. Programs “MUST NOT copy the UTC offset from a timestamp into an offset time zone in order to satisfy another program that requires a time zone suffix in its input.” Doing so, the RFC says, “will improperly assert that the UTC offset of timestamps in that location will never change.” The worked example in the RFC is worth keeping: 2020-01-01T00:00+01:00[Europe/Paris] lets a program add six months and adjust for summer time, while 2020-01-01T00:00+01:00[+01:00] produces a result off by one hour.
For a conference schedule, the practical consequence is this: if your event data stores -04:00 and not America/New_York, then a future rule change in that zone will silently shift your displayed times. If it stores the zone name, the display layer can re-resolve the offset at render time.
The database is not static
The IANA Time Zone Database is updated periodically “to reflect changes made by political bodies to time zone boundaries, UTC offsets, and daylight-saving rules,” according to the project’s own page. The current release listed there is 2026d, released 2026-09-11, and its summary includes a real example: “Canada’s Northwest Territories moved to permanent -06 on 2026-08-21.”
That is the operational shape of the problem. A jurisdiction changes its rules. The database records the change. Your runtime, your container image, your database server, and your attendees’ devices each pick up the update on their own schedule. RFC 9557 acknowledges exactly this class of inconsistency: “updates to time zone definitions being applied at different times by timestamp producers and receivers.”
You cannot control when an attendee’s phone updates its zone data. You can control whether your own stack is consistent, and you can control whether your display logic degrades gracefully when it is not.
Where the 6:00 comes from
Three code paths, three failure modes, one visible symptom.
Path one: the schedule card. A server-side template renders the event time using the server’s local zone. The server is in UTC. The card says 13:00. The attendee in Chicago sees 13:00 and assumes it is their time. They arrive at 1:00 p.m. Central. The keynote ended at noon Central.
Path two: the calendar invite. The invite is generated with a DTSTART that carries an offset but no zone name. The attendee’s calendar client applies its own zone. If the offset was correct at generation time and the zone rules have not changed, this works. If the offset was copied from a stale source, it does not.
Path three: the countdown timer. The timer computes the difference between Date.now() and a target value. If the target was parsed as a local time in the browser’s zone rather than as an instant, the countdown is wrong by the offset difference. In a browser set to America/Los_Angeles reading a target intended as America/New_York, that is a three-hour error. At 9:00 a.m. Eastern, the timer reads 6:00 a.m. Pacific and counts down accordingly.
The third path is the one that produces the headline. The first two produce quieter failures — a card that is wrong but not obviously wrong, an invite that is right until it is not.
What the formatters actually do
JavaScript’s Intl.DateTimeFormat is the standard tool for locale-aware display, and it accepts a timeZone option. The MDN reference shows the pattern directly:
new Intl.DateTimeFormat("en-GB", {
dateStyle: "full",
timeStyle: "long",
timeZone: "Australia/Sydney",
}).format(date);
// "Sunday, 20 December 2020 at 14:23:16 GMT+11"
Two things matter here. First, the timeZone option is an IANA identifier, not an offset. Second, the output includes the zone abbreviation, which is what lets an attendee verify the display against their own expectation. If your schedule card omits the zone label, you have removed the attendee’s only cheap check.
What Intl.DateTimeFormat does not do is resolve ambiguity for you. When a wall time falls in a DST gap — the spring-forward hour that does not exist — the formatter has to pick something. When it falls in a DST overlap — the fall-back hour that happens twice — it has to pick one of the two instants. The ECMAScript Internationalization API specification defines the behavior, but the behavior is not the same as your intent. If your event is scheduled at 2:30 a.m. local on a spring-forward date, the formatter will produce a time, and that time will not correspond to a moment when anyone can be awake to attend.
This is not a hypothetical for conference operations. A virtual track that runs across a DST transition — a global summit with a 24-hour broadcast window, a multi-day workshop that spans a weekend in March or November — will have at least one session scheduled in or near a transition hour. The question is whether your system knows it.
What schedulers guarantee, and what they do not
If you use a cloud scheduler to fire reminders or to start a stream, read its DST documentation before you trust it. AWS EventBridge Scheduler’s documentation is unusually clear about its behavior, and the behavior is not “do what the operator meant.”
The page states that all schedule types “invoke their targets with 60 second precision,” meaning a 1:00 schedule fires between 1:00:00 and 1:00:59. It states that EventBridge Scheduler “uses the Time Zone Database maintained by the Internet Assigned Numbers Authority (IANA),” and that the time zone is set with the --schedule-expression-timezone parameter.
Then it states the DST behavior: “When time shifts forward in the Spring, if a cron expression falls on a non-existent date and time, your schedule invocation is skipped. When time shifts backwards in the Fall, your schedule runs only once and does not repeat its invocation.”
The worked example is a cron(30 2 * * ? *) schedule in America/Los_Angeles. On spring-forward, the 2:30 a.m. invocation is skipped for that day. On fall-back, it runs once at 2:30 a.m. before the shift and does not repeat after.
For a conference, that means a reminder scheduled at 2:30 a.m. local on a transition day will either not fire or fire once. If your reminder cadence assumes it fires, you have a gap. The fix is not to avoid the scheduler. The fix is to schedule reminders at times that are not within one hour of a known transition, and to add a separate check that verifies the reminder fired.
The same page notes that rate-based schedules using days as the unit “represents a 24-hour duration on the clock,” so a rate(1 days) schedule still evaluates 24 hours after the last invocation even when the local day is 23 or 25 hours. That is a reasonable choice for a scheduler. It is a trap for an operator who assumed “daily” meant “same wall time every day.”
Windows and the mapping gap
If any part of your stack runs on Windows — a registration desk machine, a backup encoder, a sponsor’s laptop driving a slide deck — you are dealing with a second time zone model. The Windows GetTimeZoneInformation function “retrieves the current time zone settings” and returns a TIME_ZONE_INFORMATION structure. The documentation notes that “the StandardName and DaylightName members of the resultant TIME_ZONE_INFORMATION structure are localized according to the current user default UI language.”
That last sentence is the operational hazard. The display name is localized. It is not an IANA identifier. If your event data uses IANA identifiers and your Windows-side display uses the localized name, the two will not match, and any code that tries to reconcile them by string comparison will fail. The documentation also points to GetDynamicTimeZoneInformation and GetTimeZoneInformationForYear “to support boundaries for daylight saving time that change from year to year” — which is a signal that the base function is not sufficient for historical or future-dated lookups.
The practical rule: keep IANA identifiers as the canonical key in your event data, and treat any Windows-side display name as a presentation string, never as a lookup key.
A reversible fix you can test before doors open
The following procedure is a recommendation, not a finding from the sources above. It is designed to be applied to a staging copy of the schedule, tested, and reverted if it breaks anything.
Step 1: Audit the schedule model. For every time field in the event schema, classify it as instant or wall time. An instant must be stored as a UTC timestamp plus an IANA zone identifier for display. A wall time must be stored as a local date-time plus an IANA zone identifier, with no offset. If a field has an offset and no zone name, it is in the ambiguous category. Count them. That count is your risk surface.
Step 2: Pick an anchor zone and write it down. For a virtual track, the anchor zone is usually the zone where the production crew is working. For a hybrid event, it is usually the venue’s zone. Every wall time in the schedule is expressed in the anchor zone. Every instant is expressed in UTC. There is no third option.
Step 3: Make the display layer resolve, not store. The schedule card should receive an instant and a zone identifier, and call Intl.DateTimeFormat with an explicit timeZone option. It should not receive a pre-formatted string. Pre-formatted strings are the mechanism by which a server’s local zone leaks into an attendee’s browser.
Step 4: Label every displayed time. If the display shows 9:00 a.m. Eastern, it should say so. The MDN example includes the zone abbreviation in the output; use it. An unlabeled time is an invitation to misread.
Step 5: Test the transitions. Build a test fixture with four dates: the day before a spring-forward, the day of, the day before a fall-back, and the day of. For each, render the schedule card, the calendar invite, and the countdown. Compare the three. If they disagree, you have found the bug before an attendee has.
Step 6: Test the ghost times. Add a session at 2:30 a.m. local on a spring-forward date. Render it. If the system produces a time without flagging it, add a validation rule that rejects wall times inside a known DST gap. The W3C note’s definition of ghost time is the vocabulary you need for the error message.
Step 7: Verify the scheduler. If you use a cloud scheduler for reminders, list every reminder that falls within one hour of a known transition in the anchor zone. For each, either move it or add a manual check. The AWS documentation’s behavior — skip on spring-forward, fire once on fall-back — is the behavior you are working around.
Step 8: Record the rollback. Before you change the schema, export the current schedule as a flat file with all original fields. If the new model breaks a downstream integration — a sponsor portal, a registration system, a calendar feed — you can restore the original data without reconstructing it.
What to hand to the operations lead
The artifact below is a checklist for the person who owns the schedule data. It is written to be copied into a runbook or a ticket. It assumes the steps above have been completed and records the state of the system.
TIMEZONE DISPLAY CHECKLIST — VIRTUAL TRACK
Owner: Operations Lead
Event anchor zone: [IANA identifier]
Production crew zone: [IANA identifier]
1. Schedule model audit
- Fields classified as instant: [count]
- Fields classified as wall time: [count]
- Fields with offset but no zone name: [count]
- Ambiguous fields resolved: [count]
2. Display layer
- Schedule card uses explicit timeZone option: [yes/no]
- Calendar invite carries IANA zone identifier: [yes/no]
- Countdown timer parses target as instant: [yes/no]
- Every displayed time includes a zone label: [yes/no]
3. Transition tests
- Spring-forward day tested: [date]
- Fall-back day tested: [date]
- Ghost time rejected by validation: [yes/no]
- Three display paths compared: [result]
4. Scheduler
- Reminders within 1 hour of transition: [count]
- Reminders moved or manually checked: [count]
- Scheduler time zone parameter set: [value]
5. Rollback
- Original schedule exported: [path]
- Export verified readable: [yes/no]
- Restore procedure documented: [yes/no]
6. Sign-off
- Tested by: [name]
- Date: [date]
- Known gaps: [list]
Why this is worth the hour
The 9:00 keynote that was 6:00 for a third of the stream is not a formatting problem. It is a data-model problem that produces a formatting symptom. The attendees who saw 6:00 did not see a bug. They saw a schedule. They trusted it. They showed up at the time it told them.
The fix is not a new library. It is a decision about which fields are instants and which are wall times, applied consistently across the schedule card, the calendar invite, and the countdown. The sources above describe the mechanisms — RFC 9557 for attaching zone names, the IANA database for the rules, the W3C note for the vocabulary, the AWS documentation for the scheduler’s behavior, the Windows documentation for the mapping gap. The procedure is yours to test and revert.
One hour of transition testing before doors open is cheaper than one hour of an empty virtual room during the keynote.
Questions operators ask
Can I just store everything in UTC and convert at display time? For instants, yes. For wall times, no. A wall time converted to UTC and back is only correct if the zone rules have not changed between the two operations. If a jurisdiction changes its DST rules, the round trip produces a different wall time. Store wall times as wall times.
What if my event data already has offsets and no zone names? You can add zone names without changing the offsets. The offset remains valid as a cross-check. RFC 9557’s format allows both: 2026-10-14T09:00:00-04:00[America/New_York]. The zone name is what makes the value durable.
How do I know which zones have transitions? The IANA database is the reference. The W3C note’s example table shows the same instant rendered across ten zones, with dates differing by a day in some cases. If your audience spans zones, assume at least one transition is in play during your event window.
Does the countdown timer need to be exact? It needs to be consistent with the schedule card. A countdown that is off by three hours is worse than no countdown, because it actively misleads. If you cannot guarantee consistency, remove the countdown and show the labeled time instead.
What about attendees who travel during the event? Their device zone changes. If your display resolves at render time using the device zone, the displayed time changes with them. If it resolves using a stored zone, it does not. Decide which behavior you want and label it. For a virtual track, resolving to the attendee’s current zone is usually correct, but the label must say so.
Is there a way to test without waiting for a transition date? Yes. Set the system clock forward on a staging machine, or use a test fixture with hard-coded dates. The transition dates are published in the IANA database. You do not need to wait for March.