Relationships
#4357 Two forEach steps in one job run one after the other, not in parallel
Opened by mtaylor · 11/3/2026· Shipped 11/3/2026
swamp 20261022.021804.0-sha.983e2929, Windows 11.
The docs (workflow/references/execution-semantics.md, "Concurrency Limits") say step-level concurrency caps that step's forEach iterations, and job-level concurrency caps parallel steps within the job. Two independent forEach steps (dependsOn: [], each on its own model, each with concurrency: 1) in one job should then overlap. They don't: every iteration of the first step runs before the second step starts. The same steps in two jobs do overlap.
Repro (a fresh repo, two command/shell models sleeper-a and sleeper-b):
jobs:
- name: specs
steps:
- name: a-${{ self.i }}
forEach: { item: i, in: "${{ [1, 2, 3] }}" }
concurrency: 1
task: { type: model_method, modelIdOrName: sleeper-a, methodName: execute, inputs: { run: "sleep 3" } }
dependsOn: []
- name: b-${{ self.i }}
forEach: { item: i, in: "${{ [1, 2, 3] }}" }
concurrency: 1
task: { type: model_method, modelIdOrName: sleeper-b, methodName: execute, inputs: { run: "sleep 3" } }
dependsOn: []- One job: a-1, a-2, a-3 start at 18:10:46, :50, :54; b-1 only at 18:10:59. 24.7 s in all.
- The same steps as two jobs: a-1 and b-1 both start at 18:11:13. 12.3 s in all.
Expected: both forms take about 12 s. Either the step-level concurrency: 1 is applied to the whole job, or a job runs its forEach steps in sequence; if the second is intended, the docs should say so.
Shipped
Click a lifecycle step above to view its details.
zack commented 11/3/2026, 7:41:55 PM
Thanks @mtaylor for reporting this! We shipped: Give each step's concurrency its own limit. Today the level walk in WorkflowExecutionService takes the minimum concurrency of every step in a topological level and applies it to the whole level, so two independent forEach steps with concurrency 1 in one job run one iteration at a time. Replace that with per-stream gates: a step that sets concurrency gates its own iterations; steps without one share the job, then workflow, limit; SWAMP_MAX_CONCURRENT_STEPS stays a ceiling on the level total. A new gated merge in the stream layer carries the gates, and mergeWithConcurrency delegates to it. Levels with one forEach step, or with no step limits, resolve exactly as before.. The fix has been merged and a release is on its way. We appreciate your contribution to swamp.
Sign in to post a ripple.