Measured · Updated
Camunda 7, Operaton and CIB seven timer start events fire in a loop when the clocks go back: what to change before Sunday 1 November
On Sunday 1 November 2026 the US and most of Canada set their clocks back, and 01:00 to 01:59 local time happens twice. When Europe did the same on 25 October, an Operaton 2.2.0 user in Berlin reported that a timer start event with the cycle R/PT1H started "thousands of instances" in the hour after the change, "as with Camunda". Before the US change, we ran the engines of Camunda 7 Community Edition 7.24.0, Operaton 2.2.0 and CIB seven 2.2.0 through the Berlin, New York and Los Angeles changes.
In our tests, every timer start event we tried fired over and over for about an hour when the JVM ran on a zone with daylight saving time: hourly and 15-minute cycles, a cycle with an explicit UTC start, an hourly cron and a daily cron at 01:30. We then ran the real Job Executor on a PostgreSQL 16 server: an hourly timer had started 32 instances on Operaton and 88 on Camunda about 6 seconds after we began counting. On a JVM running on UTC, every timer kept its normal schedule without looping, on both databases. The three engines behaved the same wherever we ran all three.
It does not matter where your server is. What matters is the time zone the JVM runs on. In our New York runs, each burst fell somewhere between 05:00 and 07:00 UTC on Sunday (01:00 EDT to 02:00 EST), depending on when the timer last fired. Chicago is one hour later, Denver two and Los Angeles three. Make a change on Saturday at the latest.
Check this today
Your engine and version. Look for the engine jar in your build or image:
camunda-engine-7.24.0.jar,operaton-engine-2.2.0.jarorcibseven-engine-2.2.0.jarare the releases we ran. We did not run older releases. On the official Docker images and Run distributions the engine jar is packed inside another jar, so use the image tag, or ask the REST API: on the official Operaton 2.2.0 image,GET /engine-rest/versionreturned{"version":"2.2.0"}. Camunda 8 is a different engine and is not covered here.The zone your JVM runs on. With a JDK, run this as the user that runs the engine:
jcmd <pid> VM.system_properties | grep user.timezoneThe JVM takes its zone from
-Duser.timezone, then from theTZenvironment variable, then from the operating system. In our test on the official Temurin 17 image, it printeduser.timezone=for a JVM started withAmerica/ New_York -Duser.timezone=America/,New_York user.timezone=for one with onlyAmerica/ Chicago TZ=America/Chicago, anduser.timezone=Etc/UTCwith neither.UTC,Etc/UTCor a fixed offset such asEtc/GMT+5are not affected. A zone that changes its clocks, such asAmerica/New_YorkorEurope/Berlin, is. Operaton's docs add that in a cluster every node needs the same zone.The official Operaton, CIB seven and Camunda 7 Run Docker images have no
jcmd: they ship only a Java runtime. They setTZ=UTC, so they run on UTC unless you setTZor put-Duser.timezoneinJAVA_OPTS, and-Duser.timezonewins overTZ. On those images, check where you setTZandJAVA_OPTS: your compose file, Kubernetes spec ordocker runcommand. As a cross-check, the REST API lists timer jobs with their due dates (add credentials if your API needs them):curl -s 'http://localhost:8080/engine-rest/job?timers=true'Each
dueDateshows the offset at that due date, not the JVM's zone. On the official Operaton 2.2.0 image on 27 October it ended-0400withTZ=America/New_York,-0500with-Duser.timezone=America/, andChicago +0000with the image's defaults. A-0400or-0500there means the JVM is not on UTC.+0000, or no timer rows at all, does not prove the JVM is on UTC or show how it handles daylight saving time; the zone settings above do.Your timer start events. This read-only query lists them, with their schedule and next due time:
SELECT j.PROCESS_DEF_KEY_, d.JOB_CONFIGURATION_, j.DUEDATE_, j.SUSPENSION_STATE_ FROM ACT_RU_JOB j JOIN ACT_RU_JOBDEF d ON d.ID_ = j.JOB_DEF_ID_ WHERE j.HANDLER_TYPE_ = 'timer-start-event' ORDER BY j.PROCESS_DEF_KEY_;On a test engine in a New York JVM at 10:00 EDT on 30 October, with
crondailysuspended for the example, it printed this on all three engines (columns spaced for reading):crondaily CYCLE: 0 30 1 * * ? 2026-10-31 01:30:00 2 cronhourly CYCLE: 0 0 * * * ? 2026-10-30 11:00:00 1 hourly CYCLE: R/PT1H 2026-10-30 11:00:00 1 quarter CYCLE: R/PT15M 2026-10-30 10:15:00 1 utcstart CYCLE: R/2026-10-01T00:00:00Z/PT1H 2026-10-30 11:00:00 1Look at the rows with
SUSPENSION_STATE_1 (active; 2 means suspended). In our tests, everyR/cycle we tried looped, and so did crons that fire between 01:00 and 01:59 local time; a daily cron at 03:30 did not. The due time is stored without a zone, in the JVM's local time. The query does not list boundary or intermediate timers, which we did not test.Make one change before Saturday night. Each of these stopped the loop on all three engines in our H2 tests; the cycle with an explicit UTC start we tried only with the UTC JVM. On PostgreSQL we re-ran only the UTC JVM, which was clean. The other three keep due times out of the repeated hour, or keep the timers stopped through it, so we expect them to hold there too, but we have not run them on PostgreSQL. They are listed from the one that touches least.
- All your timers are crons: move them out of the repeated hour. In New York, an hourly
0 0 * * * ?became0 0 0,2-23 * * ?and a daily0 30 1 * * ?became0 30 3 * * ?. Both ran correctly on 1 November. The hourly form drops the 01:00 run every day, so change it back after Sunday; the daily one can stay. There is no such rewrite forR/cycles. They need one of the changes below. - Suspend the timer processes across the repeated hour. In New York, suspend each process definition that has a timer start event before 05:00 UTC (01:00 EDT) and activate it after 07:01 UTC (02:01 EST). For Chicago that is before 06:00 and after 08:01 UTC, Denver 07:00 and 09:01, Los Angeles 08:00 and 10:01. We tested New York, and Berlin for an hourly cycle, with the engine's Java API (
suspend, running instances not included). The timing is strict: suspending one minute late still gave a minute of repeated runs, and activating 30 minutes early gave another loop. Runs due in between are not made up, apart from one run at activation. Suspending from the REST API or Cockpit set the same suspended state. Cockpit's suspend dialog also suspends running instances unless you untick "include instances"; our clock-change runs left running instances out. While a definition is suspended, other ways of starting it are refused too: a REST start returnedProcess Definition By Id ENGINE-03043. Don't deploy a new version of those processes during the window: a new version deployed while the old one was suspended came up active. - Run the JVM on a fixed offset for the weekend. For New York, restart with
-Duser.timezone=Etc/. The sign is inverted:GMT+5 Etc/GMT+5means UTC−5, New York's winter time. Chicago would beEtc/GMT+6, DenverEtc/GMT+7and Los AngelesEtc/GMT+8; we tested only New York. Two effects to plan for. Until Sunday, crons fire on winter time, so a 09:00 cron runs at 10:00 EDT. And due times already stored are read in the new zone: in our test on H2, a time stored at 10:00 EDT was read back an hour late, while a time stored for after the change was read exactly. Because New York's winter offset is the same, a planned restart back toAmerica/New_Yorkduring the winter should read those times exactly; we did not run that switch. - Run the JVM on UTC (
-Duser.timezone=UTC). This is what Operaton's time zone page advises, and in our tests it was clean for every timer. On a new installation, start this way. On an existing one, plan it as a migration rather than a Saturday change: every time already stored is read in the new zone, and in our test on H2 a time stored at 10:00 EDT by a New York JVM was read back four hours early by a UTC JVM, and one stored for after the change five hours early. On PostgreSQL we ran UTC only on a fresh database. Crons then follow UTC, so their local time moves twice a year.
What did not help in our tests: an explicit UTC start in the cycle (
R/), switching from2026-10-01T00:00:00Z/ PT1H R/PT1HtoR/PT15M, or a cron at a time inside the repeated hour.- All your timers are crons: move them out of the repeated hour. In New York, an hourly
What we measured
We ran each timer form in its own JVM with the engine's own ClockUtil and an H2 database, from an hour before the change to an hour after, on Berlin's 25 October change and the New York and Los Angeles changes on 1 November, and in a JVM on UTC. The three engines gave the same result in every case.
| Timer start event | JVM on Berlin, New York or Los Angeles time | JVM on UTC |
|---|---|---|
R/PT1H (the reported form) | Fired over and over for an hour | Once an hour |
R/PT15M | Fired over and over for an hour | Every 15 minutes |
R/ (explicit UTC start) | Fired over and over for an hour | Once an hour |
Hourly cron 0 0 * * * ? | Fired over and over for an hour | Once an hour |
Daily cron in the repeated hour (0 30 1 * * ?; Berlin 0 30 2 * * ?) | Fired over and over for an hour | Once a day |
That is 72 of 72 runs looping on a zone with daylight saving time and 24 of 24 clean on UTC. As a control, the same forms in a New York JVM on 25 October, a date with no change there, were clean in all 15 runs. The test the Berlin user posted also failed for us on Operaton 2.2.0, unchanged.
These H2 runs stepped the engine clock forward a minute at a time and capped the runs counted per step, so they show that a timer stays stuck for the hour, not how many instances you would get. For a count, we started the real Job Executor with the engine clock held one second past an hourly timer's due time in a New York JVM and counted instances six times, about a second apart. Operaton and Camunda had each started 39 instances by the sixth count: 6.3 seconds after counting began on Camunda, and 25.9 seconds on Operaton, which includes one 20.6-second pause between counts on our test machine. Operaton in the same setup on UTC stayed at 1. That was a small in-memory test setup; your rate depends on the database, history level, executor threads and the process itself.
On PostgreSQL 16 we ran Operaton and Camunda with an hourly R/PT1H timer in New York and on UTC, each engine with the PostgreSQL driver it pins. With the real Job Executor and the engine clock held one second past the due time, Operaton had started 32 instances and Camunda 88 by the sixth count, 6.5 and 6.3 seconds after counting began; on UTC each stayed at 1. With the clock stepped forward, both engines looped too; on Operaton, a timer due at 01:40 EDT ran over and over from 05:40 to 06:00 UTC, the first time 01:40 to 01:59 comes round, then ran normally. So on PostgreSQL the loop came before the clocks went back and ended at 06:00 UTC; in our H2 runs it could also run on through the hour after. The test the Berlin user posted, which runs the job from the test code instead of the Job Executor, passed on PostgreSQL: the timer fired an hour late instead of looping. A test like that does not show whether a server database is safe.
Why it happens
The engine calculates the next due time correctly. In a New York JVM at 05:59:50 UTC on 1 November, the next run of an hourly timer is 06:59:50 UTC: 01:59:50 EST, the second time 01:59:50 comes round. But the engines' database scripts store DUEDATE_ as a date and time with no zone (timestamp, datetime or datetime2, depending on the database), so it is saved as 2026-11-01 01:59:50: the same text as the run that just fired at 01:59:50 EDT. From then on the two can't be told apart, and how the engine goes wrong depends on the database.
- On PostgreSQL, the Job Executor asks the database for jobs whose
DUEDATE_is at or before now, and the database compares the two as wall times. While the clock is still in the first 01:xx, a due time stored as 01:59:50 is already due, so the job runs again, stores the same due time and repeats until the clocks go back at 06:00 UTC. This explanation fits every value we stored and read and the engine's query text; we did not trace the query itself. - On H2, reading the value back into Java gives the first 01:59:50, which is EDT: 05:59:50 UTC, an hour early, so the job is due again at once, and in our minute-by-minute runs it stayed stuck for about an hour. A plain JDBC test on H2 showed this without the engine: the times we tried from 06:00:00 to 06:59:50 UTC came back exactly one hour early in a New York JVM. PostgreSQL's driver read those times correctly and put first-pass times an hour late instead.
A JVM on UTC stores times that never repeat, which is why it was clean on both databases. The same zone-less storage is why changing a JVM's zone on an existing database shifts the times already stored (above).
Already happened?
If you are reading this after 1 November, or Europe's 25 October change hit you: a large number of instances of one process definition started within about an hour of the change, at the same timer, points to this. Check what those instances did (emails, payments, records written), then make one of the changes above before the next one. The clocks go forward in March, which skips an hour instead of repeating one; we did not test that change.
Where the fix stands
On 27 October no release of Operaton, CIB seven or Camunda 7 Community Edition had a fix.
- Operaton: the Berlin report, #3973, was open. A maintainer replied on 26 October that it needs exploring and that requiring UTC is not ideal. On 27 October the team linked CIB seven's pull request below and noted that it covers cron syntax only. The latest release is 2.2.0.
- CIB seven: its maintainers had an open pull request, cibseven/cibseven#517, for cron timers: it skips runs in the second pass of the repeated hour. A maintainer's review on 27 October noted that cycles such as
R/PT15Mdo not go through that code. The latest release is 2.2.0. - Camunda 7 Community Edition: 7.24.0 is the latest release, and its GitHub repository is archived.
We'll update this page if a release changes the answer.
What we tested
- Camunda 7 CE 7.24.0, Operaton 2.2.0 and CIB seven 2.2.0 engines with H2 2.3.232, from Maven Central on 27 October 2026, on Eclipse Temurin 17, each run in its own JVM in a container with no network.
- Timer start events only:
R/PT1Hat two phases,R/PT15Mat two phases,R/at two phases, an hourly cron and a daily cron in the repeated hour, in2026-10-01T00:00:00Z/ PT1H Europe/Berlin,America/New_York,America/Los_AngelesandUTC, with the engine'sClockUtilstepping a minute at a time and executing due jobs from the test. - The Job Executor count on H2: Operaton and Camunda in New York, and Operaton on UTC,
R/PT1H, H2 in memory, full history, six counts about a second apart. - PostgreSQL 16.15 with Operaton 2.2.0 (driver 42.7.14) and Camunda 7.24.0 (driver 42.5.5), database and JVM in one container with no network:
R/PT1Hin New York and on UTC, from the test code and with the real Job Executor (clock held one second past the due time and counted six times; and clock stepped forward), plus a plain JDBC round trip in New York, Berlin and UTC. - The changes above: the fixed offset
Etc/GMT+5, the moved crons and the suspension window in New York (cron moves also in Berlin), and the UTC JVM, each on all three engines and each on a fresh database. - The stored-time effects of a zone change: a plain JDBC test on H2, writing from a New York JVM and reading from a UTC,
Etc/GMT+5or New York JVM. Not the engine itself. - The listing query on the three engines' own H2 schemas; the
jcmdcheck on the Temurin 17 image. - On the official Operaton 2.2.0 Docker image: the REST zone and version checks with
TZ=America/New_York, with-Duser.timezone=America/and with the defaults; and, with PostgreSQL 17 and a New York JVM, the listing query on the engine's own schema, suspension and activation from REST and with Cockpit's request, a REST start of a suspended definition, and redeploying a changed cron as a new version. None of these ran across a clock change.Chicago - Not tested: MySQL, MariaDB, Oracle, SQL Server or DB2 (their scripts use the same zone-less column types, but we have not run them); on PostgreSQL, CIB seven, timer forms other than
R/PT1H, Berlin and Los Angeles, and the changes other than a UTC JVM; boundary and intermediate timers; Chicago and Denver; Camunda 7 Enterprise releases; suspension from Cockpit or the REST API across a clock change; switching the Job Executor off across the change, which Operaton's time zone page also suggests; and a real clock change.
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.