← Back to Blog RSS

Terraform concurrency: What it is and how to control it

Terraform State Management State Locking CI/CD

Terraform concurrency refers to multiple runs executing at the same time, which without proper controls leads to state conflicts and broken deployments. Understanding how Terraform handles concurrent operations is essential for teams running infrastructure at scale.

TL;DR
$ cat terraform-concurrency.tldr
• Terraform uses state locking to prevent concurrent writes, but this only protects you if your backend supports it.
• Terraform Cloud and Enterprise enforce concurrency limits that can queue or block runs in ways that catch teams off guard.
• For resources like AWS Lambda, provisioned concurrency requires careful Terraform configuration to avoid race conditions during apply.

Terraform concurrency seems like an abstract problem until you watch two pipelines target the same infrastructure at once, one terraform plan built against stale data, the other already halfway through terraform apply.

At scale, concurrency becomes inevitable. As teams grow, workspaces multiply, and more automation starts to run Terraform in response to commits, approvals, scheduled events, and app deployments.

Concurrency becomes the default operating condition. Multiple processes are allowed to act on the same desired state concurrently, each one convinced it has exclusive access to the truth.

Even without the right guardrails, though, concurrent modifications don't always fail loudly. They show up as queued runs, lock error messages, duplicated resource creation, missing dependencies, partial apply operations, and multiple state files that now need manual repair.

We are going to unpack how Terraform concurrency actually works, where the real Terraform concurrency limit shows up in HCP Terraform and Terraform Enterprise, and why specific cases such as Lambda-provisioned concurrency need more careful dependency management than teams often expect.

What is concurrency in Terraform?

Terraform concurrency is when multiple runs attempt to execute against the same terraform state at the same time. Two engineers both run Terraform from different terminals, two CI jobs fire in parallel against one workspace, or one automated deployment overlaps with a manual apply. Essentially, more than one terraform run is trying to read, modify, or apply changes against the same infrastructure and the same state files concurrently.

Concurrency often gets confused with parallelism in Terraform, even though they're not the same thing.

Parallelism is what Terraform does inside a single run when it performs operations across independent resources simultaneously, based on the dependency graph and the configured parallelism setting.

Concurrency is what happens when separate runs overlap.

One is about how Terraform performs operations within a terraform plan or terraform apply, while the other is about whether multiple Terraform configurations, users, or processes can act on the same state safely.

Pattern Recognition

A team can reduce parallelism and still have terrible concurrency hygiene, or maximize performance inside a single apply and still stay safe if runs are properly serialized.

Terraform handles concurrent runs with state locking

Terraform's first line of defense against concurrency is state locking.

Before a run can perform operations that write state, Terraform attempts to acquire a lock so no other run can modify that same state file at the same time.

If the lock is acquired, the run proceeds. If the lock cannot be acquired, the competing run will wait for the lock or fail with an error, depending on the backend and execution settings. Either way, the solution is simple: one writer at a time.

The key factor is where your state lives.

Even the local backend can lock the file on the machine where Terraform is running, but that isn't meaningful protection for a team working across multiple laptops, runners, or ephemeral CI agents, because the lock is only local to that filesystem and does nothing to coordinate shared infrastructure across environments.

Backend choice, then, is not an implementation detail. State locking only helps when it actually matches the way your team runs Terraform. A backend that can lock state in theory, but is wrapped in a workflow that still allows competing apply operations from different places, will not save you from bad process design.

Platform-level concurrency limits shape real Terraform throughput

After state locking, the next layer is the Terraform concurrency limit imposed by the platform that runs your workloads.

In HCP Terraform, maximum concurrency is set at the organization level by your subscription plan, which means only a certain number of plan and apply operations can execute at the same time across the organization.

Terraform Enterprise is different. You control the capacity, and the default deployment starts with 10 concurrent runs, which ties run throughput directly to the memory and worker capacity you have provisioned.

Teams often scale their CI/CD systems faster than they scale their Terraform execution model. More repositories, more workspaces, more environments, and more automated triggers all sound like progress until dozens of runs hit the queue together and the system starts serializing them behind the scenes.

At that point, the issue is not whether Terraform can create more resources or make more API calls in parallel inside a run. The issue is whether your organization has enough room to let the right runs start at all.

Hitting the limit turns fast pipelines into queues

When you hit the Terraform concurrency limit, the first indicator is usually drift in delivery timing. Runs sit in a queue. Pipelines that normally finish quickly now wait for a worker. Changes that were supposed to reach production after one approval can remain pending while other workspaces consume the available run slots.

If nobody is watching run logs and queue depth, those delays can feel random, even though the system is behaving exactly as configured.

The fix is rarely to push harder on the same setup. Smaller, purpose-scoped workspaces reduce contention across multiple threads because fewer unrelated resources share the same execution surface area.

Staggered pipeline triggers help when multiple environments would otherwise run simultaneously for no operational reason. Run triggers and upstream dependency orchestration are usually safer than firing several terraform apply commands at once and hoping the queue sorts it out.

If queueing becomes a recurring issue, your concurrency model and workspace layout need attention.

Concurrent resource creation is where state conflicts become expensive

Queued runs are annoying, but concurrent resource creation is where Terraform concurrency becomes operationally expensive.

If two Terraform processes try to create, modify, or destroy the same resource at roughly the same time, without effective state locking and clear ownership boundaries, the result is not just delay. It can duplicate resources and cause conflicting updates, missing dependencies in the dependency graph, provider-level error responses, and a terraform state file that no longer matches what actually exists in the environment.

The teams most likely to encounter these results still rely on the default local workflow for shared infrastructure, or have setups where multiple Terraform configurations touch overlapping resources without explicit dependencies or cleanly separated state files.

One process may read stale data, another may modify the live object first, and by the time the second run reaches apply, the desired state it planned against is already wrong.

Observation

Terraform is deterministic only when the inputs stay stable. Concurrent modifications break that assumption.

Use a remote backend that supports real shared locking, keep ownership boundaries between workspaces and state files clear, and avoid letting multiple processes modify the same resources unless the workflow deliberately serializes them.

Lambda-provisioned concurrency exposes Terraform ordering problems quickly

You publish a function version, point an alias at it, and configure aws_lambda_provisioned_concurrency_config so Lambda keeps pre-initialized execution environments ready to serve requests without cold starts. In practice, this is one of those cases where a valid Terraform configuration can still fail if dependency ordering is not explicit enough.

Provisioned concurrency cannot target $LATEST. It must point to a published version or an alias that resolves to a published version. Terraform has to finish publishing the function version before it can successfully apply the concurrency configuration.

If those changes are introduced together, especially in a run that is also modifying related permissions, event sources, or other resources, Terraform may attempt the apply operations in an order that is technically allowed by the graph but operationally too early for Lambda's control plane to accept.

Explicit dependencies earn their keep when it comes to Lambda-provisioned concurrency in Terraform.

A carefully modeled alias, depends_on where it's genuinely needed, and a workflow that avoids changing the function and concurrency settings in the same moment all reduce the chance of race conditions.

In especially sensitive environments, separating the version publish and the provisioned concurrency update into distinct apply phases is often cleaner than trying to force one massive apply to do everything at once.

Version and alias dependencies need to be modeled explicitly

The safest pattern is to publish the function version, update an alias to reference that version, and then apply provisioned concurrency against the alias rather than wiring everything directly to an implicitly changing version string.

You give Terraform a clearer graph, Lambda a stable qualifier to target, and make rollbacks easier because the alias becomes the unit of traffic control instead of the raw version number.

When teams skip that structure, they often create a subtle timing bug. A new version is published in the same run, the provisioned concurrency resource tries to attach to what it believes is the right qualifier, and the apply either fails outright or lands against the wrong version boundary.

Terraform can manage Lambda concurrency, but if you want deterministic results, then tightly coupled operations like Lambda version publication, alias updates, and concurrency configuration all need explicit dependencies.

Best practices for managing concurrency in Terraform

Good Terraform concurrency management starts with a backend and execution model that can coordinate shared state, but it does not end there. You also need:

Use remote state and state locking as a non-negotiable baseline, understanding the maximum concurrency available in HCP Terraform or Terraform Enterprise before you scale pipeline fan-out, and keeping an eye on queue behavior in logs rather than assuming slower runs are just temporary noise.

Be honest about when explicit dependencies belong in your Terraform configuration. For normal resources, the dependency graph is often enough. For dependency-sensitive resources such as aws_lambda_provisioned_concurrency_config, a more deliberate sequence is usually the safer choice.

Design Principle

Terraform concurrency is manageable, but only when you treat it as a system design concern rather than a one-time backend setting.

Conclusion

Terraform concurrency rarely breaks infrastructure in a big way. It breaks it by creating queues nobody expected, state conflicts nobody can reproduce easily, and apply failures that only happen when the wrong runs overlap at the wrong time.

That's what makes it dangerous. The failure mode often looks like ordinary pipeline noise until you realize the system has no reliable way to control who gets to modify shared infrastructure, and when.

To keep concurrent operations from turning into hidden infrastructure debt, use state locking through a backend and execution model built for shared use, understand your Terraform concurrency limit before you scale out workspaces and automation, and model dependency-sensitive resources such as Lambda-provisioned concurrency with enough care that Terraform is not forced to guess at the order.

Stategraph gives teams the visibility and control to understand how Terraform runs execute, where contention builds, and what needs to change before concurrency becomes a production problem. Get started with Stategraph today.