One S3 bucket without DynamoDB — how OpenTofu 1.10 native locking keeps state concurrency safe
If you've ever managed infrastructure with Terraform or OpenTofu, you've probably encountered this at least once: two jobs in a CI/CD pipeline running apply nearly simultaneously, and something feels off even though the DynamoDB lock table looks fine. Or the opposite — a simple personal project where you have to manage a DynamoDB table, split IAM policies across two services, and worry about backup settings. Honestly, there are moments when you think, "Do I really need all this just for state locking?"
Starting with OpenTofu 1.10, this has changed. When AWS released conditional write support for S3 in August 2024, it laid the technical foundation for implementing atomic locks with S3 alone, and the OpenTofu team adopted an RFC in February 2025, officially supporting the use_lockfile = true parameter in 1.10. You no longer need a DynamoDB table.
This post covers why this change became possible, how the underlying mechanism works, and walks through migrating from DynamoDB locking to native S3 locking. We'll also look at practical pitfalls: exactly how race conditions can unfold in mixed-version environments, and how to identify and release stale locks.
Why DynamoDB Was Needed — and What S3 Changed
Why S3 Alone Wasn't Enough
Using DynamoDB as a lock coordinator in the past wasn't a design flaw — it was a workaround for S3's limitations at the time. S3, as an object store, didn't support atomic conditional writes: "only allow this write if the file doesn't already exist." If two processes simultaneously checked for the existence of a lock file and both tried to write, both would succeed — a classic TOCTOU (Time-of-Check-Time-of-Use) problem.
DynamoDB solved this with ConditionalExpression. A PutItem with attribute_not_exists(LockID) executes atomically, so only one of two concurrent processes succeeds. It was an architecture that combined S3's storage capability with DynamoDB's atomicity.
August 2024: S3 Changed
AWS released conditional write support for S3 in August 2024. A PUT request with the HTTP header If-None-Match: * succeeds only if the object does not already exist; if it does exist, S3 returns 412 Precondition Failed. Because this behavior is guaranteed atomically, S3 can now fulfill the role of a lock on its own.
When OpenTofu starts a tofu apply, it attempts to create a .tflock file at the same path as the state file using a conditional write. For production/terraform.tfstate, the lock file would be production/terraform.tfstate.tflock. If another process attempts the same thing simultaneously, S3 rejects the second write and OpenTofu returns "Error: state already locked."
What to Check Before Migrating
Version Requirements
The use_lockfile parameter is only supported in OpenTofu 1.10 or later (as of September 2026). There are community posts suggesting that HashiCorp Terraform has introduced the same parameter, but since OpenTofu and Terraform have evolved independently after the license fork, teams using Terraform should verify directly in the HashiCorp official changelog. The explanations in this post are based on OpenTofu 1.10.
It's important to first confirm that all team members and every execution environment in your CI/CD pipeline are running 1.10 or later. Skipping this and switching anyway means older clients won't recognize the S3 lock file, leaving concurrent execution protection only half-functional.
Checking Your S3 Bucket Policy
Conditional writes work within the existing s3:PutObject permission — no separate API is needed. However, s3:PutObject and s3:DeleteObject must be allowed for the .tflock file path. If you're using S3-compatible storage like MinIO or OVHcloud, verify that the implementation supports conditional writes (If-None-Match).
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-state-bucket",
"arn:aws:s3:::my-state-bucket/*"
]
}If you were previously using the DynamoDB approach, you can remove the entire IAM policy block containing permissions like dynamodb:GetItem, dynamodb:PutItem, and dynamodb:DeleteItem.
Migration Paths — Different Approaches by Team Size
The migration path splits into two, depending on your environment.
Why the Dual-Lock Transition Is Safe
If developers or CI/CD runners may be using different versions of the configuration simultaneously, it's safer to go through an intermediate step where both parameters are enabled at the same time. The key point here is that an OpenTofu 1.10 client seeing a backend with both use_lockfile = true and dynamodb_table set will acquire both locks.
This means the new client holds the DynamoDB lock as well, so an old client can detect that the resource is locked. Here's what that looks like:
If you skip the dual-lock phase and remove DynamoDB immediately, any 1.9 clients that haven't been upgraded yet won't recognize the use_lockfile parameter at all — they'll read and write state regardless of whether an S3 lock file exists. The 1.10 client on the other side dutifully creates a .tflock and assumes it's safe, but there is no coordination between the two clients whatsoever, and a race condition follows. The essence of this scenario is that the lock medium both sides understand disappears.
Step 1 — Enable Dual Locking
terraform {
backend "s3" {
bucket = "my-state-bucket"
key = "prod/tofu.tfstate"
region = "us-east-1"
use_lockfile = true
dynamodb_table = "tf-lock-table"
}
}At this point, update OpenTofu to 1.10 or later across all local environments and CI/CD runners on the team. Verifying in the execution logs that both locks are being acquired makes the next step safer.
Step 2 — Remove the DynamoDB Parameter
terraform {
backend "s3" {
bucket = "my-state-bucket"
key = "prod/tofu.tfstate"
region = "us-east-1"
use_lockfile = true
}
}After changing the configuration, reinitialize the backend.
tofu init -reconfigureStep 3 — Final Verification Before Deleting the DynamoDB Table
Before deleting the table, it's worth confirming that state is still accessible normally.
tofu state list
tofu planIf everything looks good, remove the DynamoDB table resource. If the table was managed by Terraform/OpenTofu, remove it from state and then delete the actual table.
How to Find and Release Stale Locks
If a process terminates abnormally, the .tflock file may be left behind in S3. If the error message on the next run includes a lock ID, you can use that value directly.
tofu force-unlock <lock-id>The tricky scenario is when a process died quietly, or you need to clean up a lock left by an old job. In that case, the fastest approach is to find the lock file directly in S3 and inspect its contents.
aws s3 ls s3://my-state-bucket/ --recursive | grep '\.tflock$'
aws s3 cp s3://my-state-bucket/prod/tofu.tfstate.tflock - | jqThe .tflock file is JSON and contains fields like ID, Who, and Created. Once you have the lock ID, you can release it with force-unlock, or if you're certain the lock owner is gone, you can delete the file directly. Either way, confirm there are truly no active processes before proceeding.
Trade-offs — What to Expect and What to Watch Out For
| Item | Details |
|---|---|
| Simplified infrastructure | S3 handles both storage and locking; eliminates multi-service dependency |
| Cost | DynamoDB On-Demand costs vary by team size and apply frequency, but the lock table itself is usually negligible. The practical benefit is fewer things to manage, not dramatic cost savings |
| Simplified IAM | The entire DynamoDB permission block can be removed |
| Reduced management overhead | No more DynamoDB table monitoring, backups, or capacity management |
| Latency | Fewer cross-service calls; reduced latency compared to calling a separate service |
Key things to watch out for:
Managing the dual-lock transition period — As described above, removing DynamoDB while 1.9 clients still exist eliminates the shared coordination medium. Keep the transition period as short as possible, and decide in advance how you'll verify that the upgrade is complete.
S3-compatible storage caution — For S3-compatible storage like MinIO or OVHcloud, verify separately whether conditional writes (If-None-Match) are supported. AWS S3 itself supports this in all regions since August 2024.
Don't confuse this with S3 Object Lock — The similar names are easy to mix up, but S3 Object Lock is a WORM (Write Once Read Many) compliance feature. It has a completely different purpose from state locking. The native locking discussed here is based on S3 Conditional Writes.
Behavior with Terragrunt and Atlantis
If you're using Terragrunt, you can specify use_lockfile in the config of the remote_state block like any other backend parameter. Since Terragrunt simply proxies the S3 backend, the rendered backend configuration is passed directly to OpenTofu. However, depending on the Terragrunt version, there may be logic that filters certain parameters, so it's safer to confirm via execution logs that the parameter is actually being passed through.
Atlantis works naturally without any special support. It's not Atlantis that manages the lock — it's the OpenTofu 1.10+ binary that Atlantis executes, which sees use_lockfile = true in the backend configuration and uses S3 locking on its own. So rather than "Atlantis automatically leverages this," a more accurate description is: "as long as the backend configuration is correct, S3 locking doesn't conflict with the Atlantis execution flow."
Making the Call Today
As of 2026, community consensus is moving toward infrastructure simplification, and some managed platforms have begun officially removing DynamoDB lock tables from their own customer infrastructure. The model where S3 handles both storage and locking is becoming the new default.
Of course, if you're dealing with a legacy environment, using S3-compatible storage, or your team's OpenTofu upgrade is still in progress, there's no need to rush the migration. But for new projects, starting with use_lockfile = true from the beginning is the natural choice.
Deleting one DynamoDB table won't change the world, but the subtle burden of "having to manage two services just for state locking" disappearing is surprisingly satisfying.