You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] Nondeterminism replaying local activity markers recorded after a FORCE_CLOSE_COMMAND workflow task failure (1.34.0) #1976
A workflow fans out ~900 regular activities with asyncio.gather. A workflow outbound interceptor records progress with a local activity: at most one is in flight, and when it resolves a done callback starts the next one with the changes accumulated in the meantime. (We have replaced it with a regular activity as a workaround.)
Describe the bug
While results streamed in, workflow tasks took 2.5-6 s and the server failed them 12 times with WORKFLOW_TASK_FAILED_CAUSE_FORCE_CLOSE_COMMAND (51-139 activity completions arrived during each task). After one of those, the next workflow task (started 08:52:05.021Z, completed 08:52:08.319Z) recorded, in this order:
local activity markers seq 1769 (activation_index 2) and 1770 (activation_index 3)
The complete_time of 1296 and 1719 is before that task started, so both ran during the force-closed attempt (08:52:00.674Z-08:52:04.941Z) and their results were handed to the next attempt.
Any later full replay of this history fails with:
[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1719)
Replaying the downloaded history with Replayer, the same workflow code and temporalio 1.34.0 fails the same way, so the SDK cannot replay a history it wrote. On the live worker the workflow task kept failing until we terminated the workflow (114 attempts over 16 hours).
Our reading (may be wrong): in the original activation 1, the 1296 result was delivered after the 422 activity resolutions, so the next local activity got seq 1719. activation_index keeps 1296 in activation 1 but not its position among the other resolutions in that activation, so on replay the next local activity gets a different seq.
Minimal Reproduction
We don't have a minimal reproduction yet. The history contains customer data, so we can't attach it, but we can share sanitized event and marker metadata or run a patched build against it.
Shape: asyncio.gather over many activities, plus an outbound interceptor that chains one local activity at a time from done callbacks, plus enough concurrent completions to trigger FORCE_CLOSE_COMMAND (default history.maximumBufferedEventsBatch = 100).
Thanks for reporting. A history file (even sanitized) and/or repro code would definitely help investigation. If you are a Temporal Cloud customer, I'd also recommend opening a support ticket on our portal, and provide information through that channel.
Also, can please answer these questions?
Was this workflow/interceptor/local activity working fine before, or is this a new workload?
If that looks like a regression, are you saying it appeared in 1.32.0? What other versions of the SDK have you tried?
Do workflow executions that fail with NDE ran exclusively on SDK 1.32.0, or did they start executing with a different version and then transitioned to a newer SDK version?
Hi, I was able to reproduce this failure locally with a fully synthetic workload (no customer data). The attached zip contains the reproducer scripts, the histories referenced below and SHA-256 checksums.
Setup: Temporal CLI 1.10.0 (dev server, Server 1.32.2, default history.maximumBufferedEventsBatch=100), Python 3.13. Each run used exactly one SDK version from start to finish.
Workload (repro_peritem.py): 900 parallel chains of 2 regular activities each (1800 total). A workflow interceptor schedules one local activity per completed regular activity, strictly serially: the done callback of the running local activity starts the next one.
Live runs:
SDK 1.34.0, runs that hit WORKFLOW_TASK_FAILED_CAUSE_FORCE_CLOSE_COMMAND: 2 of 2 failed replay with "[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1209)" and "(1724)". The live workflow also stopped progressing (timed out after 300 s).
SDK 1.34.0, run without force close: 1 of 1 replayed fine.
SDK 1.35.0, runs with force close: 4 of 4 completed live and replayed fine. One further run without force close also replayed fine.
A variant that batches all pending results into one local activity (only about 8 local activities in total) replayed fine on both versions, including runs with force close.
Cross-version replay of the same frozen histories:
Failing 1.34.0 histories replayed with 1.34.0: same error.
Failing 1.34.0 histories replayed with 1.35.0: same error (LocalActivity(1209) / LocalActivity(1724)).
Passing 1.35.0 history replayed with 1.34.0 and 1.35.0: OK.
For these two failing histories, upgrading the replayer from 1.34.0 to 1.35.0 does not resolve the replay failure. In contrast, the tested passing history generated under 1.35.0 replays successfully with either version.
This suggests that the observed difference involves how the histories are produced during execution, rather than a replay-only fix in 1.35.0. However, these tests do not distinguish between a history-generation issue, a replay limitation shared by both versions, or an interaction between the two. The root cause remains unverified.
Limits: small number of runs; I have not identified the change between the two versions; the controls "no interceptor" and "regular instead of local logging activities" never reached force close, so they are not valid controls yet. This is a synthetic reproduction of the reported pattern, not a reproduction of the original history.
Follow-up with captured worker logs. The attached repro_1976_frozen_v2.zip contains the synthetic reproducer, worker logs, histories and checksums.
I re-ran the same synthetic workload with Core log output captured. Each run is a separate worker process running exactly one workflow, and the error lines carry the same run_id as the exported history.
Python SDK 1.34.0, run 1: FORCE_CLOSE_COMMAND occurred. The log shows 2x "Trying to send a new WFT for a run which already has one!" (14:44:58 and 14:44:59 UTC), then about 9 s later the live worker fails the workflow task with "[TMPRL1100] Nondeterminism error: Missing associated machine for LocalActivity(1502)". Replay of the exported history fails with the same error class.
Python SDK 1.34.0, run 2: same pattern. 5x the same log line between 14:50:00 and 14:50:17 UTC, then at 14:50:28 the live failure "Missing associated machine for LocalActivity(1781)". Replay fails as well.
Python SDK 1.35.0, one comparison run: FORCE_CLOSE_COMMAND occurred, the log line was not observed, and replay passed.
The log line matches the task-replacement scenario documented in temporalio/sdk-rust#1640 ("Buffer a new workflow task while the run already has one"). That PR buffers an incoming workflow task while Core still holds the current task, including while waiting for a local activity. The Python SDK 1.35.0 release notes list this fix (Core commit 212e8150).
This makes #1640 my leading hypothesis for the synthetic reproduction. It is not a confirmed diagnosis of the original production case in this issue: #1640 documents a different downstream nondeterminism error, and my comparison changes the full SDK version rather than isolating that fix (1.35.0 also contains other Core changes, e.g. #1620). I have not tested a 1.35.0-based build with only the #1640 buffering change reverted.
Separately, the two failing 1.34.0 histories from my previous comment also failed replay under 1.35.0. I keep that result distinct from the successful replay of histories newly generated under 1.35.0.
Could you help confirm whether this reproducer exercises the #1640 path and whether that task replacement explains the LocalActivity marker replay failure?
Working before, or a new workload? A fairly new workload. The interceptor that chains local activities was added to our code on 2026-09-14, about a month before the failure. Our dependency was unpinned (temporalio>=1.29,<2), so that code only ever ran on 1.32.0 through 1.34.0, and we did not notice this error before 2026-10-15. So we can't say it worked on an older SDK; we never ran it on one.
Regression in 1.32.0? Which versions did you try? The production failure was on 1.34.0 only, and I replayed that history with 1.34.0 (same error). I could not share that history, so I used Luca's synthetic reproducer to compare versions: 1.31.0, 1.32.0, 1.34.0 and 1.35.0 (not 1.33.0). The results below point to a regression that starts in 1.32.0 and is fixed in 1.35.0.
One SDK version, or a version transition? One version. The whole execution, from start to the first NDE, ran on a single worker build with 1.34.0, so this is not a history from a mixed-version run. The same holds for every reproducer run below: each used one SDK version from start to finish.
I ran Luca's repro_peritem.py (case A, n=900) against a local dev server (CLI 1.8.1, Server 1.31.2), two runs per SDK version. For each history I counted local activity markers whose complete_time is earlier than the start of the workflow task that recorded them. Every run had FORCE_CLOSE_COMMAND failures (6 to 14 per run).
SDK
"already has one" log lines (run 1 / run 2)
markers completed before their WFT started
replay
1.31.0
0 / 0
0 / 0
ok / ok
1.32.0
7 / 8
122 / 321
ok / NDE
1.34.0
4 / 14
48 / 672
NDE / ok
1.35.0
0 / 0
0 / 0
ok / ok
These markers look the same as the ones in our production history. Markers 1296 and 1719 had a complete_time before the recording WFT started, and an original_schedule_time after their complete_time. In other words, local activities that ran during one workflow task were recorded by the next one.
That fits temporalio/sdk-rust#1640. temporalio/sdk-rust#1442 (first shipped in 1.32.0) made a run that waits on a local activity look idle to Core, so a new workflow task arriving at that point replaced the outstanding one. #1640 (shipped in 1.35.0) buffers it instead. In our case the replacing task is the server's retry of the force-closed workflow task, not a query.
My reading:
Affected: 1.32.0 through 1.34.0, for workflows that are waiting on a local activity when another workflow task arrives (here, the retry after FORCE_CLOSE_COMMAND).
Fixed in 1.35.0 for new executions.
Histories already written by an affected version can still fail to replay on 1.35.0. I confirmed Luca's result with the two failing histories above (one from 1.32.0, one from 1.34.0). We terminated our stuck execution.
Not every affected history fails replay: one 1.32.0 run and one 1.34.0 run replayed fine even with misplaced markers. That is probably why this depends so much on load and timing.
If it helps, I can open a PR with a regression test for this path (a force-closed workflow task while a local activity is running), in whichever repo you prefer, sdk-python or sdk-rust.
Activity
mjameswh commentedon Oct 16, 2026
Thanks for reporting. A history file (even sanitized) and/or repro code would definitely help investigation. If you are a Temporal Cloud customer, I'd also recommend opening a support ticket on our portal, and provide information through that channel.
Also, can please answer these questions?
tconley1428 commentedon Oct 22, 2026
Feel free to reopen if more information becomes available.
lucafranco1994pot-cloud commentedon Nov 5, 2026
repro_1976_frozen.zip
Hi, I was able to reproduce this failure locally with a fully synthetic workload (no customer data). The attached zip contains the reproducer scripts, the histories referenced below and SHA-256 checksums.
Setup: Temporal CLI 1.10.0 (dev server, Server 1.32.2, default history.maximumBufferedEventsBatch=100), Python 3.13. Each run used exactly one SDK version from start to finish.
Workload (repro_peritem.py): 900 parallel chains of 2 regular activities each (1800 total). A workflow interceptor schedules one local activity per completed regular activity, strictly serially: the done callback of the running local activity starts the next one.
Live runs:
Cross-version replay of the same frozen histories:
For these two failing histories, upgrading the replayer from 1.34.0 to 1.35.0 does not resolve the replay failure. In contrast, the tested passing history generated under 1.35.0 replays successfully with either version.
This suggests that the observed difference involves how the histories are produced during execution, rather than a replay-only fix in 1.35.0. However, these tests do not distinguish between a history-generation issue, a replay limitation shared by both versions, or an interaction between the two. The root cause remains unverified.
Limits: small number of runs; I have not identified the change between the two versions; the controls "no interceptor" and "regular instead of local logging activities" never reached force close, so they are not valid controls yet. This is a synthetic reproduction of the reported pattern, not a reproduction of the original history.
Happy to run additional variants if that helps.
lucafranco1994pot-cloud commentedon Nov 5, 2026
Follow-up with captured worker logs. The attached repro_1976_frozen_v2.zip contains the synthetic reproducer, worker logs, histories and checksums.
I re-ran the same synthetic workload with Core log output captured. Each run is a separate worker process running exactly one workflow, and the error lines carry the same run_id as the exported history.
The log line matches the task-replacement scenario documented in temporalio/sdk-rust#1640 ("Buffer a new workflow task while the run already has one"). That PR buffers an incoming workflow task while Core still holds the current task, including while waiting for a local activity. The Python SDK 1.35.0 release notes list this fix (Core commit 212e8150).
This makes #1640 my leading hypothesis for the synthetic reproduction. It is not a confirmed diagnosis of the original production case in this issue: #1640 documents a different downstream nondeterminism error, and my comparison changes the full SDK version rather than isolating that fix (1.35.0 also contains other Core changes, e.g. #1620). I have not tested a 1.35.0-based build with only the #1640 buffering change reverted.
Separately, the two failing 1.34.0 histories from my previous comment also failed replay under 1.35.0. I keep that result distinct from the successful replay of histories newly generated under 1.35.0.
Could you help confirm whether this reproducer exercises the #1640 path and whether that task replacement explains the LocalActivity marker replay failure?
repro_1976_frozen_v2.zip
kkdy21 commentedon Nov 5, 2026
Sorry for the late reply, and thanks @lucafranco1994pot-cloud for the reproducer.
Answers to @mjameswh's questions first:
temporalio>=1.29,<2), so that code only ever ran on 1.32.0 through 1.34.0, and we did not notice this error before 2026-10-15. So we can't say it worked on an older SDK; we never ran it on one.I ran Luca's
repro_peritem.py(case A, n=900) against a local dev server (CLI 1.8.1, Server 1.31.2), two runs per SDK version. For each history I counted local activity markers whosecomplete_timeis earlier than the start of the workflow task that recorded them. Every run had FORCE_CLOSE_COMMAND failures (6 to 14 per run).These markers look the same as the ones in our production history. Markers 1296 and 1719 had a
complete_timebefore the recording WFT started, and anoriginal_schedule_timeafter theircomplete_time. In other words, local activities that ran during one workflow task were recorded by the next one.That fits temporalio/sdk-rust#1640. temporalio/sdk-rust#1442 (first shipped in 1.32.0) made a run that waits on a local activity look idle to Core, so a new workflow task arriving at that point replaced the outstanding one. #1640 (shipped in 1.35.0) buffers it instead. In our case the replacing task is the server's retry of the force-closed workflow task, not a query.
My reading:
If it helps, I can open a PR with a regression test for this path (a force-closed workflow task while a local activity is running), in whichever repo you prefer, sdk-python or sdk-rust.