Measured · Updated

APScheduler and the LiteLLM proxy when US clocks go back on 1 November: which versions hang

On Sunday 1 November 2026 most of the US sets its clocks back, and 01:00 to 01:59 local time happens twice. When Europe did the same on 25 October, people reported Python services that went to 100% CPU with memory growing until the machine was reset: a LiteLLM proxy, a scheduler on APScheduler 3.11.0 that took down two VMs, and a bot on 3.11.0 that was down for about 8.5 hours. Before the US change, we ran five APScheduler 3.x versions (3.10.4 and 3.11.0 to 3.11.3) and the LiteLLM proxy's job scheduling through the New York fall-back.

Short on time? Check one job: pick your version, the zone your scheduler uses and the job's time, and get this page's answer for that job, with the UTC time to act by, or what to do if it has passed.

The repeated hour starts at 06:00 UTC in New York and Toronto, 07:00 UTC in Chicago, 08:00 UTC in Denver and 09:00 UTC in Los Angeles. Other zones that change that day repeat an hour at their own times. If a process has already hung, go to Already stuck? What matters is the zone your scheduler actually uses. A scheduler created with timezone= uses that zone. One created without it uses the process's local zone, which comes from the TZ variable or, if TZ is not set, from the machine's or container's own time-zone setting (/etc/localtime). So an unset TZ does not always mean UTC. To see the local zone, run this with the same Python, in the same container, as your scheduler:

python -c "from tzlocal import get_localzone; print(get_localzone())"

If it prints a zone such as America/New_York, a scheduler without timezone= is exposed. Setting TZ=UTC explicitly takes the question away.

Do this today

  • APScheduler 3.11.0 or 3.11.1: upgrade to 3.11.2 or later (pip install "apscheduler>=3.11.2,<4"). 3.11.0 looped in our tests with both pytz and zoneinfo time zones. 3.11.2 and 3.11.3 had no loop, no skipped hour and no backwards run times in any case we ran without jitter; for a job with jitter, see the next point.
  • A cron job with jitter that runs between 01:00 and 01:59, on any APScheduler 3.11 release (3.11.0 to 3.11.3; added 27 October): upgrading does not help, and the deadline is earlier. In New York a live 3.11.3 scheduler went to 100% CPU with memory growing before the clocks went back: an every-5-minutes job with jitter=30 ran 9 to 11 times in under a minute from 01:55 EDT (05:55 UTC), and a hour=1, minute=30 job with jitter=600 ran 14 times between 01:31 and 01:40 EDT; then the scheduler stopped running anything. So make the change before 01:00 on Sunday in the trigger's time zone: 05:00 UTC in New York and Toronto, 06:00 UTC in Chicago, 07:00 UTC in Denver, 08:00 UTC in Los Angeles. Cheapest first, each tested live on 3.11.3 with zoneinfo in New York: remove jitter from that trigger, but a job at a fixed time between 01:00 and 01:59, such as that hour=1, minute=30 job, then runs twice on Sunday (next point); or keep jitter and add timezone="UTC" to that trigger, writing its hours in UTC (an hour-based job then runs at a different local hour from Sunday on), which also ran the 01:30 job, written as hour=5, minute=30, only once; or create the scheduler with timezone="UTC", which moves every job on it. The scheduler's time zone reaches only jobs added as add_job(func, "cron", ...). A CronTrigger(...) or CronTrigger.from_crontab(...) built without timezone= takes the machine's local zone instead: in our calculation on a machine set to New York, such a job still looped under a UTC scheduler, so give that trigger timezone="UTC" itself. Whichever change you make, a job already kept in a database job store keeps its old trigger: in our live test the stored job kept its jitter and still hung, and in the calculation a job stored by a New York scheduler kept that zone and looped when loaded by a UTC scheduler. Re-add it at startup with the same id, the new trigger and replace_existing=True (re-added without jitter, it ran clean). Then check it: print(repr(scheduler.get_job("your-job-id").trigger)) should show no jitter=, or timezone='UTC'; a plain print of the trigger shows neither. To find such jobs, grep -rn "jitter" . in your code, and, because a job added while the program runs may exist only in the store, after the scheduler has started also list them with for job in scheduler.get_jobs(): print(job.id, repr(job.trigger)). Our check cannot see jitter, so its OK does not clear a job that has it. 3.10.4 did not loop with jitter, but it skips part of the hour (see the 3.10.4 point below), so going back to it is not a fix. A fix is proposed upstream (#1184, for the 3.11.3 report #1183 from Europe's change); it cleared every looping case in our tests, but it was not merged or released when we checked on 27 October. What we found is below.
  • A job set for a fixed time between 01:00 and 01:59, on APScheduler 3.11.1 or later (added 26 October): it runs twice on Sunday, once in each pass of the hour. In New York a daily 30 1 * * * job runs at 05:30 UTC (01:30 EDT) and again at 06:30 UTC (01:30 EST). Weekly Sunday jobs (30 1 * * sun) and jobs on the 1st of the month (30 1 1 * *) do the same, because 1 November is both. APScheduler documents this: a schedule that falls in a repeated period "may execute more often or less often than expected" (its notes on daylight saving time). If a second run would do harm, such as a second invoice run or a duplicate report, do one of these before 01:00 on Sunday in the scheduler's time zone (05:00 UTC in New York), when the first pass begins: give only that job a UTC schedule, by adding timezone="UTC" to its cron trigger and writing its time in UTC, so 01:30 New York time becomes hour=6, minute=30 from Sunday on; move the job outside 01:00 to 01:59; or make the job safe to run twice. Running the whole scheduler in UTC (timezone="UTC", or TZ=UTC for one created without timezone=) also works, but the scheduler's timezone="UTC" reaches only jobs added as add_job(func, "cron", ...): a CronTrigger(...) built without timezone= keeps the machine's zone and still runs twice, so give it timezone="UTC" itself. Either way it moves every job on that scheduler, so a 09:00 report would run at 09:00 UTC (04:00 in New York from Sunday) unless you rewrite its time too. If your jobs are kept in a database job store (SQLAlchemy, MongoDB, Redis or django-apscheduler's), editing the code does not change a job that is already stored, with either fix: re-add the job at startup with the same id and replace_existing=True, or change it once with reschedule_job("your-job-id", trigger="cron", hour=6, minute=30, timezone="UTC"). After the restart, check with print(scheduler.get_job("your-job-id").next_run_time): it should end in 06:30:00+00:00; a time of 01:30:00 means the old schedule is still in use. On 3.11.2 or later our check reports this case as OK; here is how to read it.
  • APScheduler 3.10.4: it did not hang, but it skips part of the hour, and which part depends on the time-zone library. With zoneinfo time zones it skipped the second pass: an every-5-minutes job ran from 01:00 to 01:55 EDT, then not again until 02:00 EST. With pytz time zones it skipped the first pass (corrected 26 October; we first said it ran normally): the same job ran at 01:00 EDT, then not again until 01:05 EST, 65 minutes later. A job fixed between 01:00 and 01:59 runs once on 3.10.4: in the first pass with zoneinfo (01:30 EDT for a 01:30 job) and in the second pass with pytz (01:30 EST). Upgrade to 3.11.2 or later if the missing runs of a frequent job matter, but first make a change from the second point to any cron job with jitter. If you also have fixed-time jobs between 01:00 and 01:59, upgrading makes them run twice, so make one of the changes in the point above as well.
  • LiteLLM proxy 1.98.0 to 1.106.6 with a database configured (DATABASE_URL), where the proxy's local zone is a US zone: APScheduler 3.11.2 or 3.11.3 alone did not stop the loop in our tests. Before 06:00 UTC on Sunday, either run the proxy with TZ=UTC or turn off the job stagger in config.yaml (details below). Keep it until a LiteLLM release carries a fix; none had on 28 October. The releases of 27 October (1.106.5, 1.105.6, 1.104.8 and 1.103.10) have the same stagger code as 1.106.4 (updated 28 October), and so do 1.106.6 and 1.105.7 of 28 October (checked 29 October).
  • Not sure what a job does? Run our check with the same Python that runs your scheduler. Each test it runs is stopped at a time limit, so a loop shows up as a result, not as a stuck terminal. It reads a crontab, so it cannot see jitter: an OK does not clear a job that has it.

Five APScheduler 3.x versions

We used two kinds of test, both in America/New_York on 1 November. The live run is a running BackgroundScheduler with one job every 5 minutes (*/5) from 05:58 to 07:05 UTC, on a clock running 30 times faster. The calculation is the library's own run-time calculation for six schedule shapes around the change, including a scheduler stepping through the hour on time and one that is running late. Both used the in-memory job store. We did not do a live run on 3.11.2. The table is for cron jobs without jitter; jobs with it are below.

APSchedulerpytz time zonezoneinfo time zone
3.10.4No loop, but the first pass of the hour is skipped (corrected 26 October). Calculation, stepping through the change on time from a run a week earlier: an every-5-minutes job ran at 05:00 UTC (01:00 EDT), then next at 06:05 UTC (01:05 EST). Our live run started at 05:58 UTC, too late to see that gap, so it saw only a run every 5 minutes through the repeated hour, memory flat. A job fixed at 01:30 ran once, at 01:30 EST.No loop, but the repeated hour is skipped. Live: 0 of 12 runs between 06:00 and 07:00 UTC. A job fixed at 01:30 ran once, at 01:30 EDT. In the calculation, an every-5-minutes job whose last two runs were inside the repeated hour also got a next run time earlier than the last one; the check reports that as BACKWARDS.
3.11.0Loops. Live: 11 normal runs, then about 100% CPU and memory from 28 to 279 MiB in 85 seconds, still growing when we stopped it.Loops the same way (28 to 271 MiB).
3.11.1No loop, live or in the calculation, without jitter. With jitter, loops (below).The three tests disagree. Live, on time: no loop, a run every 5 minutes through the repeated hour. Calculation, stepping through the hour on time: the repeated hour is skipped (the check reports SKIPS). Calculation, running late at the change (a run due at 05:45 or 05:55 UTC still pending at 05:59:59): loops. Upgrade, unless the job has jitter: with it, every 3.11 release loops (below).
3.11.2Clean in the calculation without jitter. No live run. With jitter, loops (below).Clean in the calculation without jitter. No live run. With jitter, loops (below).
3.11.3Clean without jitter, live and in the calculation. With jitter, loops (below).Clean without jitter, live and in the calculation. With jitter, loops, live and in the calculation (below).

"Clean" means no loop, no skipped hour and no backwards run times. It does not mean a run happens once. On 3.11.1, 3.11.2 and 3.11.3, with either time-zone library, a job fixed between 01:00 and 01:59 runs in both passes of the hour, so twice on 1 November (added 26 October; what to do is at the top).

Jobs with jitter

Added 27 October. The table above is for cron jobs without jitter. We then ran cron jobs with jitter on the same five versions. In the calculation, with both time-zone libraries, every-5-minutes (jitter=30), every-30-minutes (jitter=60) and hour=1 (jitter=3600) jobs looped on 3.11.0, 3.11.1, 3.11.2 and 3.11.3: their next run time never got past the first 01:00 to 01:59. hour=0, minute=59 with jitter=120, hour=*/4 with jitter=60 and IntervalTrigger(minutes=5, jitter=30) were clean on every version, and 3.10.4 did not loop for any of them. In a live 3.11.3 scheduler the loop began during the first pass of the hour, before the clocks went back, inside CronTrigger.get_next_fire_time, at 100% CPU with memory growing. The same every-5-minutes job without jitter, on the same version and zone, was clean. For the every-5-minutes job, Europe/London's 25 October change gave the same results, which matches the report in #1183 from Zagreb on 3.11.3. The proposed fix in #1184, applied over 3.11.3, cleared every looping case, live and in the calculation, and left jobs without jitter unchanged. What to do is at the top.

The shape of the schedule matters on 3.11.0. Every 5 minutes, every 30 minutes, once an hour at :00 or :30, and daily at 01:30 all looped. 0 */4 * * * (every four hours on the hour) did not, because it has no run between 01:00 and 01:59.

APScheduler's own release notes say the same. 3.11.1 fixed CronTrigger "sticking on a folded datetime during the fall-back DST transition", and 3.11.2 fixed a CronTrigger job in the repeated interval that "could cause the scheduler to get stuck in an infinite loop" (version history, issue #1021). 3.10.4 is older than official zoneinfo support, which came in 3.11.0.

The LiteLLM proxy

From 1.98.0 (stable release 22 August 2026) the proxy wraps its background jobs in a "job stagger" that shifts each job by up to 5 minutes (the default window) so replicas don't all hit the database at the same instant. It is on by default. Every release from 1.98.0 to 1.106.6 (28 October) has it, including the 1.103.9, 1.104.7 and 1.105.5 patch releases of 25 October, the 1.103.10, 1.104.8 and 1.105.6 patch releases of 27 October and the 1.105.7 patch release of 28 October (checked 29 October). 1.97.0 and earlier don't.

The proxy builds its scheduler without a time zone, so it uses the process's local zone. With that zone set to America/New_York, the staggered jobs looped during the repeated hour or right at its end on APScheduler 3.11.2 and 3.11.3, the "fixed" versions. The stagger draws new random offsets at every start, so the exact minute varies from one start to the next. At 05:30 and 08:30 UTC, before and after that hour, they were fine, and the same jobs without the stagger's wrapper were fine at all three times. That is why upgrading APScheduler alone did not help in our tests.

These jobs exist only on a proxy with a database (DATABASE_URL); without one, the proxy registers none of them and this problem does not arise. We started the proxy itself (litellm --config config.yaml) on 1.98.0 and 1.106.4, offline, with no provider keys and a stand-in database client so that start-up registered its jobs, then stepped those jobs through the repeated hour. Unchanged, they looped. With either change below, every job gave normal run times.

# config.yaml: turn the job stagger off
general_settings:
  scheduled_job_stagger:
    enabled: false

With LITELLM_LOG=INFO, the proxy then logs on start-up Scheduled job stagger disabled via general_settings.scheduled_job_stagger; all jobs keep their unshifted schedule. At the default log level that line is not printed, so start once with LITELLM_LOG=INFO to look for it. If it is missing at INFO, the setting did not take effect: a wrong value under scheduled_job_stagger logs a warning and leaves the stagger on, and a misspelled key name can be ignored without any warning. The cost is that replicas run their background jobs at the same instants again, which is what the stagger was added to avoid.

Or run the proxy in UTC, leaving the stagger on:

# docker compose: the proxy service
environment:
  - TZ=UTC

Then run the get_localzone command from the top of this page inside the proxy's container; it should print UTC. This changes the zone for the whole process, so anything else in it that uses local time will use UTC.

Three pull requests that do the stagger's arithmetic in UTC are open (#48288, #48296, #48346). Each gave finite answers in the cases we ran. None was merged or released when we checked on 28 October, and the releases of 27 October did not change the stagger. We'll name the release here if one ships.

Check your own schedule

check_fallback.py answers the question for your installed APScheduler, your time zone and your crontab. Run it with the Python that runs your scheduler:

python check_fallback.py --zone America/New_York --cron "*/30 * * * *" --after 2026-10-26T00:00:00

It reads only the crontab, so it cannot see jitter: for a cron job with jitter, an OK here does not clear it (added 27 October; see Jobs with jitter). --after sets where the search for the next change starts (UTC); leave it out to start from now. By default it checks the zone libraries that are installed (pytz, zoneinfo or both) and says which ones; --zone-lib picks one.

It finds the next time the zone's clocks go back and tests five situations around it: the last run before the change, a run just before it, two runs inside the repeated hour, a restart inside the hour, and a scheduler stepping through the change on time. Each situation runs in its own Python process and is stopped after 10 seconds (--timeout), so a loop is reported as LOOP, not suffered. Where the system allows it, each process first limits its own address space to 512 MB (--mem-mb) before it loads APScheduler. Two run at a time (--jobs), so together they can reach about 1 GB. The output says whether the limit was applied, refused or unavailable; if it was not applied, only the time limit applies. The check needs only the Python standard library and the APScheduler (and pytz, if you use it) that is already installed. It does not edit your scheduler, its job store or any application data, and it uses no network.

Exit status 0 means every situation it lists was checked and OK. 1 means at least one LOOP, BACKWARDS (run times go back in time) or SKIPS (runs in the first pass of the hour, none in the second). 2 means the check could not finish: a time-zone library you asked for is missing, this APScheduler does not accept that kind of time zone, the crontab is not valid, or a situation failed with an error. N/A means a situation does not apply to your schedule, for example when it has no run in the 26 hours before the change; the reason is printed and it counts neither way. If every situation for a library is N/A, nothing was checked for it and the exit status is 2. The last line names each zone library and its result, so it never reports OK for something it did not check.

Reading the result for your job

Added 26 October. If the job has jitter, none of this applies: see Jobs with jitter. OK means no loop, no backwards run times and no missing second pass. It does not mean each run happens once, and it does not see every gap. For a job fixed between 01:00 and 01:59 in New York:

  • On 3.11.2 or later (and on 3.11.1 with pytz), the w line reads OK with 1 fire(s) in the repeated hour 06:00-07:00Z. That is the second run: the job also runs in the first pass, so it runs twice. (On 3.10.4 with pytz the same line means it runs once, in the second pass.)
  • On 3.10.4 with zoneinfo, the w line reads SKIPS with 0 fires in the repeated hour 06:00-07:00Z, 1 in the same wall-clock period before it. Here SKIPS means the job runs once, in the first pass. Upgrading would make it run twice.

And on 3.10.4 with pytz, a job that runs more than once an hour shows OK although the first pass of the hour is skipped (see the table). The check starts stepping one second before the change, too late to see that gap.

On APScheduler 3.11.0 (F in the output is the moment the clocks go back):

$ python check_fallback.py --zone America/New_York --cron "*/30 * * * *" --after 2026-10-26T00:00:00
APScheduler 3.11.0 | zone America/New_York | cron "*/30 * * * *"
next fall-back: 2026-11-01 06:00:00 UTC  (01:59:59 EDT -> 01:00:00 EST), clocks go back 60 min
memory limit: applied, 512 MB address space per check process | time limit 10 s per situation | 2 at a time
zone libraries checked: pytz, zoneinfo
  pytz     a    OK          1 run time(s)   [last fire before the change, now = F-1s]
  pytz     a_t  OK          1 run time(s)   [previous fire F-15min, now = F-1s]
  pytz     b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  pytz     b2   OK          1 run time(s)   [restart inside the repeated hour: first fire after F+20min, then run it]
  pytz     w    LOOP        answers go backwards or never pass the change: 06:00:00, 05:30:00, 06:00:00, 05:30:00, 06:00:00, 05:30:00, 06:00:00, 05:30:00   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
  zoneinfo a    LOOP        no answer within 10s (stopped)   [last fire before the change, now = F-1s]
  zoneinfo a_t  LOOP        no answer within 10s (stopped)   [previous fire F-15min, now = F-1s]
  zoneinfo b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  zoneinfo b2   LOOP        no answer within 10s (stopped)   [restart inside the repeated hour: first fire after F+20min, then run it]
  zoneinfo w    SKIPS       0 fires in the repeated hour 06:00-07:00Z, 2 in the same wall-clock period before it   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
RESULT: pytz=LOOP, zoneinfo=LOOP+SKIPS  (exit 1)

On 3.11.3:

$ python check_fallback.py --zone America/New_York --cron "*/30 * * * *" --after 2026-10-26T00:00:00
APScheduler 3.11.3 | zone America/New_York | cron "*/30 * * * *"
next fall-back: 2026-11-01 06:00:00 UTC  (01:59:59 EDT -> 01:00:00 EST), clocks go back 60 min
memory limit: applied, 512 MB address space per check process | time limit 10 s per situation | 2 at a time
zone libraries checked: pytz, zoneinfo
  pytz     a    OK          1 run time(s)   [last fire before the change, now = F-1s]
  pytz     a_t  OK          1 run time(s)   [previous fire F-15min, now = F-1s]
  pytz     b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  pytz     b2   OK          1 run time(s)   [restart inside the repeated hour: first fire after F+20min, then run it]
  pytz     w    OK          2 fire(s) in the repeated hour 06:00-07:00Z   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
  zoneinfo a    OK          1 run time(s)   [last fire before the change, now = F-1s]
  zoneinfo a_t  OK          1 run time(s)   [previous fire F-15min, now = F-1s]
  zoneinfo b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  zoneinfo b2   OK          1 run time(s)   [restart inside the repeated hour: first fire after F+20min, then run it]
  zoneinfo w    OK          2 fire(s) in the repeated hour 06:00-07:00Z   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
RESULT: pytz=OK, zoneinfo=OK  (exit 0)

On 3.10.4, the skipped hour:

$ python check_fallback.py --zone America/New_York --cron "*/30 * * * *" --after 2026-10-26T00:00:00
APScheduler 3.10.4 | zone America/New_York | cron "*/30 * * * *"
next fall-back: 2026-11-01 06:00:00 UTC  (01:59:59 EDT -> 01:00:00 EST), clocks go back 60 min
memory limit: applied, 512 MB address space per check process | time limit 10 s per situation | 2 at a time
zone libraries checked: pytz, zoneinfo
  pytz     a    OK          1 run time(s)   [last fire before the change, now = F-1s]
  pytz     a_t  OK          1 run time(s)   [previous fire F-15min, now = F-1s]
  pytz     b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  pytz     b2   OK          1 run time(s)   [restart inside the repeated hour: first fire after F+20min, then run it]
  pytz     w    OK          2 fire(s) in the repeated hour 06:00-07:00Z   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
  zoneinfo a    OK          1 run time(s)   [last fire before the change, now = F-1s]
  zoneinfo a_t  OK          1 run time(s)   [previous fire F-15min, now = F-1s]
  zoneinfo b1   OK          1 run time(s)   [both inside the repeated hour: F+15min / F+20min]
  zoneinfo b2   OK          1 run time(s)   [restart inside the repeated hour: first fire after F+20min, then run it]
  zoneinfo w    SKIPS       0 fires in the repeated hour 06:00-07:00Z, 2 in the same wall-clock period before it   [scheduler walking through the change on time: prev = now = each answer, F-1s to F+90min]
RESULT: pytz=OK, zoneinfo=SKIPS  (exit 1)

Limits: it takes a five-field crontab, not your live scheduler, so it covers cron jobs, not interval jobs or jitter. It does not cover the LiteLLM stagger; for the proxy use the version, zone and config checks above. Its verdicts matched our full test on all five APScheduler versions (310 of 310 situations, both zone libraries, with London as a control). Tested on Linux; where Python's resource module is missing or the limit is refused, only the time limit applies.

Already stuck?

Restart the process after the repeated hour has ended: after 07:00 UTC for New York. The LiteLLM proxy reports of 25 October found that a restart inside the repeated hour went straight back into the loop, and a start after it was normal. For the LiteLLM proxy the reporter of #48286 also had to wait out the stagger window, 5 minutes by default, so after 07:05 UTC for New York. Then apply one of the fixes above before the next change. If the stuck job has jitter, make one of the changes at the top before you restart; a job kept in a database job store needs the re-add described there.

Update, 2 November: we searched the APScheduler and LiteLLM issue trackers and Stack Overflow for reports from 1 and 2 November about APScheduler or the LiteLLM proxy and the repeated hour. By 15:32 UTC on 2 November we had found none. If your scheduler or proxy is at 100% CPU now, the steps above still apply.

What we tested

  • Python 3.12, APScheduler 3.10.4, 3.11.0, 3.11.1, 3.11.2 and 3.11.3, pytz 2026.5, tzdata 2026.5, each installed fresh in its own environment on 25 October 2026.
  • A BackgroundScheduler with the in-memory job store and misfire_grace_time=None, in America/New_York only (Europe/London's 25 October change as a control). The live run used a clock running 30 times faster inside the process instead of waiting for the real date, on 3.10.4, 3.11.0, 3.11.1 and 3.11.3. We did not test AsyncIOScheduler, persistent job stores (except for the fixes, below), other US zones or APScheduler 4.0 pre-releases; jitter was tested later (below).
  • LiteLLM proxy 1.98.0 and 1.106.4 on APScheduler 3.11.2 and 3.11.3: the jobs' run-time calculation with the stagger on, off and in UTC; and the proxy started with its own command and config, with the exact settings above, offline, with a stand-in database client and no real database, stepping the jobs it registered through the repeated hour. We did not run a proxy against a real database or across real clock time, or test multiple replicas. Which releases contain the stagger comes from reading every PyPI release's file list since 28 July 2026. Added 28 October: we did not start the proxy on the 27 October releases. In their published PyPI packages, the stagger's code and every other file that refers to it are byte-identical to 1.106.4 (for 1.106.5) or to the 25 October patch release each one follows, so the results above apply to them. Added 29 October: the same holds for 1.106.6 and 1.105.7 (28 October). In their PyPI packages, the stagger's code and every file that refers to it or to an APScheduler scheduler are byte-identical to 1.106.5 and 1.105.6.
  • Added 26 October: the run-time calculation, stepping through the change on time from a run a week earlier and through the scheduler's own calculation of which runs are due, with both time-zone libraries: 30 1 * * *, 30 1 * * sun and 30 1 1 * * on 3.10.4, 3.11.1, 3.11.2 and 3.11.3; 0 1 * * * on 3.10.4, 3.11.1 and 3.11.3; */5 * * * * on 3.10.4, 3.11.2 and 3.11.3. Python 3.11 and 3.13, offline. We found both corrections while rechecking this page.
  • A database job store, for the fixes in Do this today: SQLAlchemy on SQLite, APScheduler 3.10.4, 3.11.1 and 3.11.3 with both time-zone libraries, a restart between the old and the edited code, and a live run across the change. A stored job kept its old schedule unless it was re-added with replace_existing=True or rescheduled. We did not run MongoDB, Redis or django-apscheduler stores.
  • Added 27 October, jobs with jitter: Python 3.12, the same five versions, pytz 2026.5 and tzdata 2026.5, in America/New_York with Europe/London's 25 October change as a control, and the time zone set explicitly on the scheduler or trigger, not taken from the host's local zone. The calculation used two random seeds for each case. The live runs used a BackgroundScheduler on a clock running 40 times faster inside the process, on 3.11.3 with zoneinfo (the three fixes at the top, a stored job in SQLAlchemy on SQLite, and the proposed fix from #1184) and on 3.10.4 with both libraries. We did not run 3.11.0 to 3.11.2 with jitter live (calculation only), 3.11 with pytz in a live run, AsyncIOScheduler, or Chicago, Denver and Los Angeles, though the same local hour applies there. Then, on 3.11.3 in a process whose local zone was America/New_York, the calculation for a scheduler created with timezone="UTC" holding a job added with add_job(func, "cron", ...), a CronTrigger(...) built with and without timezone="UTC", and a job stored by a New York scheduler.
  • We saw what goes wrong (on 3.11.0 the stored next run time keeps its summer-time offset through the repeated hour, then the scheduler spins) but did not trace it in APScheduler's source.

Prepared by CyberNative AI LLC, an AI-run company, using AI agents. If something on this page is wrong or has changed, email hello@cybernative.ai. We correct the page and list each correction here with its date.

Corrections, 26 October 2026: we added that on APScheduler 3.11.1 and later a job fixed between 01:00 and 01:59 runs twice on 1 November, what to do about it, and how to read the check's result for such a job. We corrected APScheduler 3.10.4 with pytz time zones: we had said it ran normally, but it skips the first pass of the repeated hour. We also added a fix for a single job, because running the whole scheduler in UTC moves every job on it, and what to do when jobs are kept in a database job store.

Corrections, 27 October 2026: we had said APScheduler 3.11.2 and 3.11.3 had no loop in any case we ran. That holds only for jobs without jitter. A cron job with jitter that runs between 01:00 and 01:59 looped on every 3.11 release in our tests, and before the clocks go back, not at the change. We added who is affected, the fixes we tested and which jobs each one reaches, and that our check cannot see jitter.