How do you prevent concurrent pipeline runs on the same pipeline in Azure DevOps?
Quick Answer
Azure DevOps provides the exclusive lock check on Environments and the batch trigger setting to prevent concurrent runs. The exclusive lock ensures only one deployment job targets an environment at a time, queuing others. The batch: true trigger setting coalesces rapid commits into a single build. For non-deployment pipelines, you can use the pipeline settings UI to limit parallel jobs to one.
Detailed Answer
Think of concurrent pipeline prevention like a single-lane bridge with a traffic light. Multiple cars (pipeline runs) want to cross, but the bridge can only handle one at a time. Without traffic control, two cars enter simultaneously and collide (conflicting deployments corrupt state). The traffic light ensures one car crosses completely before the next enters, and cars that arrive during a crossing queue in order. Azure DevOps provides several mechanisms that act as this traffic light for different scenarios.
The exclusive lock check is the primary mechanism for preventing concurrent deployments. You add it as a check on an Environment in Azure DevOps (Project Settings, Environments, select the environment, Approvals and Checks, add Exclusive Lock). When a deployment job targets that environment, the exclusive lock check ensures only one pipeline run can actively deploy to that environment at a time. If Pipeline Run A is deploying to payments-production and Pipeline Run B reaches the same environment, Run B waits in a queue until Run A completes. The lock supports two behaviors: sequential (queued runs execute in order) and runLatest (only the most recent queued run executes, older queued runs are canceled).
The batch trigger setting addresses a different problem: rapid successive commits triggering multiple builds simultaneously. When batch: true is set in the trigger configuration, commits that arrive while a build is already running are batched together. Instead of starting a new build for each commit, Azure DevOps waits for the current build to finish and then starts a single new build incorporating all commits that arrived during the previous run. This reduces build queue congestion and prevents the scenario where five developers pushing within a minute spawn five parallel builds that all compete for the same resources.
Pipeline settings in the Azure DevOps UI provide a simpler but coarser control. Under Pipeline Settings for a specific pipeline, you can configure the maximum number of parallel runs. Setting this to 1 ensures only one run of that pipeline executes at any time, with additional triggers queuing. This approach works for non-deployment pipelines where environment locks do not apply (like CI-only pipelines that run tests). However, this is an all-or-nothing setting that applies to all stages of the pipeline, which may be overly restrictive if you want parallel builds but serialized deploys.
For pipelines that deploy to infrastructure using stateful tools like Terraform, concurrency control is especially critical. Terraform acquires a state lock during plan and apply operations, and a second concurrent run attempting to lock the same state file will fail. While Terraform's own locking prevents corruption, the pipeline still fails messily with lock timeout errors. Using Azure DevOps exclusive locks prevents the second pipeline from even starting the Terraform steps, providing a cleaner user experience with clear queuing rather than mid-execution failures.
The production gotcha is choosing between sequential and runLatest lock behavior. Sequential guarantees every triggered pipeline run eventually executes, which matters for release pipelines where each version must deploy (audit trail requirements). RunLatest cancels intermediate runs and only deploys the latest, which is appropriate for continuous deployment where only the newest code matters. Choosing incorrectly can either create a long queue of stale deployments (sequential when you want latest) or skip mandatory intermediate releases (runLatest when audit requires every version). Another issue is lock scope: the exclusive lock applies per-environment, so if your pipeline deploys to multiple environments in sequence, you need locks on each, and the lock on environment B does not prevent concurrent access to environment A.