At 2:00 AM on 1 November 2026, DST ends for Canada. However, DST will not end in British Columbia, Alberta, the Northwest Territories, and Manitoba starting from this shift.
Each of the above announced at separate times that they will stop moving their clocks.
| Where | Zone | Decided | Days before 1 November | First tz release with it |
|---|---|---|---|---|
| British Columbia | America/Vancouver |
announced 2 March, in effect 9 March | 244 | 2026b, 22 April |
| Alberta | America/Edmonton |
in effect 18 June | 136 | 2026c, 8 July |
| Northwest Territories | America/Inuvik |
in effect 21 August | 72 | 2026d, 11 September |
| Manitoba | America/Winnipeg |
announced 17 September | 45 | none |
The politicians call it permanent daylight time. As usual, this means a whole big mess for all computer systems. In the tz database, this is a new standard time: Vancouver becomes MST, Edmonton and Inuvik become CST, and Manitoba stays on UTC-5 all year.
The tz project asks the government for a year's notice, as the updates can take a long time to propagate. Manitoba gave six weeks. As I write this, 3 of the 4 are included in the newest release 2026d. The project has said it will need a new release that will reflect all of them in the near future.
Now, anyone who has worked with timezone and date systems can attest to this, but dealing with timezones and DST is always a nightmare. Changes like this always break things. I assumed time changes like these were rare, so I went through the history to check how uncommon it actually is. The easiest way to check was to go through the release list of tz. Turns out, it is not as rare as I thought. There have been 31 releases since 2020a, and 26 of them changed future timestamps. Palestine is responsible for seven of these, Morocco is four, Fiji and Greenland at three each, and then a long list of places that show up only once. Some of them almost didn't give a warning, like Mexico's end of DST observation in 2022f, two days before it was to take effect. Lebanon decided to delay the DST start in March 2023 by a month, and then called it off 5 days later. I checked the tz releases against the official announcements to see if there was a delay, but tz released the same day as the official announcement most of the time.
Why I looked
I don't remember where I first saw the news, but I read something along the lines of "Parts of Canada to not observe DST" so I clicked on it out of curiosity. I run a scheduler and crons in Go so any DST or time-related changes naturally mean something will break. We naturally store the timestamps in UTC and each customer gets a time zone field. I checked, and we don't have any customers in the affected regions so this doesn't quite affect me this time, but the same could not be said for everyone else who works on schedulers and crons across the globe. I was curious as to how this would be fixed, was it a simple case of "just update your tzdata"? But turns out it's a lot messier than I thought.
Where tzdata comes from
Go uses the tzdata built into the OS, which is something different for every system. Python's zoneinfo does the same, and so does .NET on Linux and Ruby's tzinfo, unless you install the data gem. Anything in C gets the same files through glibc.
On my Mac, /usr/share/zoneinfo is symlinked to /var/db/timezone/zoneinfo and Apple keeps it updated, cat /usr/share/zoneinfo/+VERSION says 2026c on 20th September. On a normal Linux box, tzdata comes from the distro's tzdata package and it updates anytime someone runs a package update.
The second way is the runtime carrying its own copy and ignoring the OS. Node gets its timezones from ICU, which is compiled into the node binary, and it does not care about /usr/share/zoneinfo at all. This means installing a newer Node runtime rather than updating your base image or OS tzdata.
Containers are where this gets messy. The image has whatever /usr/share/zoneinfo its base image had on the day it was built. Some slimmer versions like Alpine don't ship any tzdata. I know this since I had built cron images that errored out with unknown time zone America/New_York. I had to then figure out what went wrong and fix it by importing _ "time/tzdata".
Databases do their own thing. Postgres ships its own copy of the tz database in its source tree. What tz database it uses depends on how it was built. The configure flag, --with-system-tzdata=/usr/share/zoneinfo makes it read the OS files instead. Postgres built without the above flag will update the tz ruleset from minor releases. The minor release from 13 August follows 2026c, which has British Columbia and Alberta. The next minor release will be on 12 November, which is after the DST change.
MySQL is a weird one. Named zones like America/Vancouver don't exist as files. They are stored in a table in the mysql schema. The table is populated by running mysql_tzinfo_to_sql /usr/share/zoneinfo. It's a copy of whatever the OS had at the time of running. The official Docker Image does the loading for you in the entrypoint, but only when initialising a new database.
The shift
The whole problem is one question: what UTC time is 9 AM in Vancouver on 1 December?
This fits in two lines of Go.
loc, _ := time.LoadLocation("America/Vancouver")
dec := time.Date(2026, 12, 1, 9, 0, 0, 0, loc)
The answer depends on which tzdata LoadLocation found. I wrapped those two lines in a small program that prints a 9 AM job for all four zones, on 15 October and on 1 December, and ran it twice. First against the system zoneinfo, which on this Mac is 2026c:
a 09:00 local job, before and after the change
America/Vancouver 15 Oct -> 16:00Z PDT -7 1 Dec -> 16:00Z MST -7
America/Edmonton 15 Oct -> 15:00Z MDT -6 1 Dec -> 15:00Z CST -6
America/Inuvik 15 Oct -> 15:00Z MDT -6 1 Dec -> 16:00Z MST -7
America/Winnipeg 15 Oct -> 14:00Z CDT -5 1 Dec -> 15:00Z CST -6
Then against the copy that go1.25.4 ships with, which is 2025b, by running it with ZONEINFO=$(go env GOROOT)/lib/time/zoneinfo.zip:
a 09:00 local job, before and after the change
America/Vancouver 15 Oct -> 16:00Z PDT -7 1 Dec -> 17:00Z PST -8
America/Edmonton 15 Oct -> 15:00Z MDT -6 1 Dec -> 16:00Z MST -7
America/Inuvik 15 Oct -> 15:00Z MDT -6 1 Dec -> 16:00Z MST -7
America/Winnipeg 15 Oct -> 14:00Z CDT -5 1 Dec -> 15:00Z CST -6
Look at Vancouver on 1 December. It's 16:00Z on the new data and 17:00Z on the old, so with old data the 9 AM job goes off when the clock in Vancouver says 10. Edmonton is the same. Inuvik and Winnipeg come out identical in both runs, and both are wrong, because 2026c doesn't know about the Northwest Territories, and no release includes Manitoba yet.
The 15 October column is the same in both. The difference comes after 1 November, which is the date when the new rules start to apply.
A newer Go doesn't fix it
My first guess was that a newer Go version would fix it, since Go embeds its own tz database when you import time/tzdata but that wouldn't work. Turns out, the embedded copy is simply a fallback. This is zoneinfo_unix.go:
var platformZoneSources = []string{
"/usr/share/zoneinfo/",
"/usr/share/lib/zoneinfo/",
"/usr/lib/locale/TZ/",
"/etc/zoneinfo",
}
$ZONEINFO is checked first, then those four directories, then the embedded database, then $GOROOT/lib/time/zoneinfo.zip. So if the image has a /usr/share/zoneinfo, that's what you get, however old it is and however new the binary is. An image keeps the tzdata it was built with. The embedded copy only matters in something like FROM scratch where there are no system files at all.
And the embedded copy is old anyway. go1.25.4 embeds 2025b. Releases don't necessarily pick up new tzdata. The newest release as I write this is go1.27.1, from 28 August, and it embeds 2026c: British Columbia and Alberta, no Northwest Territories, no Manitoba. go1.26.8 embeds 2025c, which has none of the four. The annoying part is that Go won't tell you what it loaded, there's no version string at runtime. However, process.versions.tz exists in Node and tzdata.IANA_VERSION exists in Python.
How our scheduler works
We store the first occurrence of a recurring event as a UTC timestamp. We find this to be easier than converting it to cron or any other format. Calculating the next occurrence is a simple add 7 days to it. We first check whether a timezone observes DST or not, by checking IsDST() on 1st January and 1st July. If DST is observed, we take the first occurrence UTC timestamp and the current occurrence timestamp, convert them to user's timezone and calculate the UTC offset difference. We subtract the difference from the current one. Since everything is measured against the first occurrence, it doesn't matter whether the first occurrence fell in DST or in standard time.
func CalculateDSTOffset(timezone *time.Location, firstOccurrence, currentOccurrence time.Time) (dstOffset time.Duration) {
_, firstOccurrenceZone := firstOccurrence.In(timezone).Zone()
_, currentOccurrenceZone := currentOccurrence.In(timezone).Zone()
if firstOccurrenceZone != currentOccurrenceZone {
return time.Duration(currentOccurrenceZone-firstOccurrenceZone) * time.Second
}
return 0
}
dstOffset := dst.CalculateDSTOffset(userTimezone, firstOccurrence, currentOccurrence)
currentOccurrence = currentOccurrence.Add(-dstOffset)
Previously, we had an engineer running a script that went through all of our recurrences and shifted them for DST. This was very annoying and easy to get wrong. I have heard horror stories of a 36+ hour workday when things went wrong. This was before I joined the company. I am very proud of this one, automating away the entire DST cycle.
The fix
We don't have any customers in any of the four regions, but here is how I would fix it if we did.
First thing to do would be to add unit tests to the above implementation, and unsurprisingly, the outcome depends on the tz release being used.
scheduler sees 2026c
America/Vancouver created 12 Jan, stored 17:00Z 15 Oct -> 09:00 1 Dec -> 09:00
America/Vancouver created 6 Jul, stored 16:00Z 15 Oct -> 09:00 1 Dec -> 09:00
scheduler sees 2025b
America/Vancouver created 12 Jan, stored 17:00Z 15 Oct -> 09:00 1 Dec -> 10:00 !
America/Vancouver created 6 Jul, stored 16:00Z 15 Oct -> 09:00 1 Dec -> 10:00 !
On the old tz, we are an hour late from November 1st. However, with the above solution, the code itself works out correctly. We check IsDST() before applying the offset but my implementation has the check based on 2025 dates. The check passes since Vancouver still had DST in 2025. I ran zdump -v -c 2026,2028 America/Vancouver | grep "Nov 1" to see the transition and sure enough, the gmtoff stays the same. This means CalculateDSTOffset will return 0 and no adjustment will be applied.
America/Vancouver Sun Nov 1 08:59:59 2026 UT = Sun Nov 1 01:59:59 2026 PDT isdst=1 gmtoff=-25200
America/Vancouver Sun Nov 1 09:00:00 2026 UT = Sun Nov 1 02:00:00 2026 MST isdst=0 gmtoff=-25200
The fix for us is rebuilding all of our images with the latest tz rules, where head -1 /usr/share/zoneinfo/tzdata.zi says 2026d. Then do it again, because Manitoba isn't in any release yet and whatever gets rebuilt today is still wrong for Winnipeg.
Some of you using Node or MySQL may not have it so easy.
Closing
I really wish DST didn't exist, and it seems my wish is coming true. At least with 4 Canadian time zones. Here is to hoping the rest of the world will follow, but ideally with advance notice of at least a year.
Top comments (1)
A regression test I’d add is ‘09:00 in the named zone’ across several future occurrences, rather than asserting only a stored UTC instant. Run it against the tzdata in the actual deployment image, including dates on both sides of a rule change. That catches a subtle drift: the scheduler’s code can stay unchanged while a rebuilt container computes a different future UTC time.