Terraform State in 2026: S3 Locking Replaced DynamoDB, What Else Changed
The Terraform failures I have spent the most time untangling were rarely bad resource definitions. They were state problems: two people applying from two copies of the same state, a lock nobody would admit to holding, a refactor that planned to destroy a database because an address changed. After fourteen years of this, the tooling around Terraform state has changed more in the last two years than in the previous ten: S3 can lock state on its own, the DynamoDB table everyone created out of habit is deprecated, OpenTofu encrypts state client-side, and HCP Terraform bills per resource. This post explains what Terraform state is, which backend to pick in 2026, how S3 native locking with use_lockfile works, why state is a secret, and how to split it so one mistake does not take everything with it.
What is Terraform state and why does it exist?
Terraform state is a JSON file that maps each resource address in your configuration (aws_s3_bucket.logs) to a real object in a provider (arn:aws:s3:::acme-logs), plus every attribute Terraform last saw on that object. Without it, Terraform cannot tell "this bucket already exists and I own it" from "this bucket happens to exist". Every plan is a three-way comparison between configuration, state and what the provider API reports right now.
Three fields matter when things go wrong. serial increments on every write. lineage is a UUID assigned when the state is first created and never changes, so two files with different lineages are unrelated. version is the state format, still 4 in current releases. Terraform uses serial and lineage to refuse dangerous writes: the state push docs say it will not push a different lineage, and will prevent a push when the destination has a higher serial. I tried this on Terraform 1.12 in a scratch directory and got Failed to write state: cannot import state with serial 1 over newer state with serial 3, which is exactly the guard you want when a stale copy is on someone's laptop.
State also needs a lock. Two concurrent applies against the same state will each read serial N and each write N+1, and whichever lands second wins. The state locking docs are blunt that you can disable locking with -lock=false but should not. Every serious backend now locks; the question in 2026 is only how.
Which Terraform backend should you use in 2026?
For most teams the answer is the object store of the cloud you already run in, with versioning on. The locking mechanism differs per backend, and that is the part worth understanding before the day a lock sticks.
| Backend | How it locks | Notes | Source |
|---|---|---|---|
| S3 | .tflock object created with a conditional write (If-None-Match: *) when use_lockfile = true |
Locking is opt-in; DynamoDB locking deprecated in Terraform | Terraform S3 backend |
| GCS | Lock object written only if it does not exist (DoesNotExist precondition) |
Docs say only that locking is supported; enable Object Versioning | Terraform GCS backend, source |
| azurerm | Blob lease on the state blob | Docs describe it as Blob Storage native capabilities | Terraform azurerm backend |
| HCP Terraform | Workspace lock managed by the service | Per-resource pricing, free tier capped at 500 resources | HCP Terraform overview |
| local | OS file lock plus .terraform.tfstate.lock.info |
Fine for experiments, never for shared infrastructure | force-unlock docs |
All three big object stores now lock with a primitive of the store itself, one fewer resource to bootstrap and secure. GCS and Azure have done this for years; S3 was the odd one out, which is why a generation of tutorials started with a DynamoDB table.
Whatever you choose, turn on object versioning. Both the S3 and GCS backend docs say it is "highly recommended", for recovery after accidental deletion and human error. Versioning is the only undo button state has, and I walk through using it in the sibling guide on fixing Terraform state lock errors and recovering state.
How S3 native locking with use_lockfile replaced DynamoDB
The enabling change was in S3, not Terraform. On 20 August 2024 AWS added conditional writes: a PutObject with If-None-Match: * succeeds only if no object exists at that key. That is a compare-and-swap, and a compare-and-swap is all a lock needs.
Terraform 1.10.0 (27 November 2024) added S3 native locking, noting that when combined with DynamoDB, locks are acquired from both. 1.11.0 (27 February 2025) made it generally available and deprecated the DynamoDB arguments. The current S3 backend page says DynamoDB-based locking "is deprecated and will be removed in a future minor version". OpenTofu shipped the same feature in 1.10.0 (24 June 2025), but its S3 backend docs take a different line: native locking is preferred, yet dynamodb_table is not deprecated and the team says it has no plans to deprecate either option.
Mechanically, the backend writes a small JSON object next to your state with the key plus .tflock (prod/network/terraform.tfstate.tflock), containing the same lock info you see in error messages: ID, operation, who and when. I read the S3 backend client source for 1.16.6 to confirm: the lock is a PutObject with IfNoneMatch: "*", and on failure Terraform reads the existing lock file to show you who holds it. Unlock deletes the object.
The migration is a config change, done in two applies so nobody running an older config can sneak past the lock:
# Terraform >= 1.11.0 (S3 native locking GA) or OpenTofu >= 1.10.0.
terraform {
required_version = ">= 1.11.0"
backend "s3" {
bucket = "acme-tfstate-prod" # versioning enabled on this bucket
key = "network/terraform.tfstate" # lock object: network/terraform.tfstate.tflock
region = "eu-west-1"
encrypt = true # SSE for both state and lock objects
use_lockfile = true # S3 conditional-write lock
# Phase 1: keep the old table so older configs still contend for the same lock.
# Phase 2 (after every pipeline and laptop runs this config): delete this line,
# run terraform init -reconfigure, then retire the table.
dynamodb_table = "terraform-locks"
}
}
The IAM change is small and easy to forget: the docs list s3:GetObject, s3:PutObject and s3:DeleteObject on the lock file, while s3:DeleteObject is not needed on the state file itself. If your CI role is scoped to exactly */terraform.tfstate, the first locked run will fail with an access error on the .tflock key.
What you lose with DynamoDB is also worth knowing. The S3 backend stored an MD5 digest of the state in the table and refused to read state whose checksum did not match, a guard from the days when S3 was eventually consistent. S3 has been strongly consistent since December 2020, so the digest mostly produced confusing state data in S3 does not have the expected content errors after someone edited state by hand. I will not miss it.
Is Terraform state a security risk?
Yes, and sensitive = true does not change that. HashiCorp's sensitive data guide says state is stored in plaintext including secret values, that sensitive values are still written to state and plan files, and that sensitive only redacts CLI and UI output. Database passwords, generated private keys and provider tokens all end up in the JSON unless you take steps.
Those steps, in the order I apply them:
- Keep secrets out of state. Ephemeral values and write-only arguments (Terraform 1.11+, OpenTofu 1.11+) pass a value to the provider without persisting it. I covered the HCL in my infrastructure as code 2026 roundup, along with published scans that found readable state files in public buckets.
- Encrypt what remains. The S3 backend's
encryptoption gives server-side encryption, which protects against a stolen disk and nothing else: anyone withs3:GetObjectreads plaintext. OpenTofu goes further with client-side state and plan encryption usingpbkdf2,aws_kms,gcp_kms,azure_vault,openbaoor an external key provider, withenforced = trueto refuse unencrypted writes. HCP Terraform's answer is Hold Your Own Key, generally available since September 2025, which encrypts state and plan files before upload. - Stop sharing whole states. The
terraform_remote_statedocs warn that anyone who can read the outputs can read the full snapshot, and say not to use it with sensitive data. Publish the handful of values another stack needs to SSM Parameter Store or a similar store instead, or usetfe_outputson HCP Terraform. - Treat read access to the bucket as admin access. Lock the bucket policy to the CI role and a break-glass role, block public access, and log reads.
The secrets angle connects to a broader pattern I expect to see more of this year in secrets sprawl: the leaked credential is rarely in the code, it is in an artefact the code produced.
What does HCP Terraform cost in 2026?
HCP Terraform charges per resource under management. The pricing page lists Essentials at $0.10, Standard at $0.47 and Premium at $0.99 per managed resource per month. The free tier is limited to 500 managed resources, one concurrent remote run and unlimited users. The old user-based free plan is gone: Scalr reports that HashiCorp emailed legacy organisations on 15 December 2025, and that on 31 March 2026 they were moved to the current free tier without losing workspaces. That is a secondary source; I could not find a HashiCorp announcement.
The arithmetic is simple and sometimes surprising. A modest platform of 5,000 managed resources is $500 a month on Essentials and $4,950 a month on Premium. An S3 bucket with native locking gives you managed state and locking for roughly nothing; what you pay HCP for is the run queue, policy and audit trail around it.
How should you split Terraform state?
One state per environment per component is the starting point I recommend: prod/network, prod/data, prod/app, and the same set for staging. The reasons are operational, not aesthetic.
- Blast radius. A
terraform destroyor a bad refactor can only touch what is in that state. The agent-driven deletion I wrote about in my IaC roundup removed a VPC, cluster and database in one go because they shared a root module and a state. - Lock contention. One state means one lock. A team of ten applying application changes should not queue behind a 20-minute network plan.
- Plan time. Every plan refreshes every resource in state. Terraform 1.17, at release candidate 2 as of 14 October 2026, adds a
-minimal-refreshoption that only refreshes resources with proposed changes, which helps, but a smaller state is still faster. - Permissions. Separate states let you give the app pipeline write access to
prod/appand read access to nothing else.
Do not split too finely: every cross-state reference is a dependency Terraform cannot see. I stop when two pieces always change together.
Moving resources between states used to mean terraform state mv with -state-out. Inside one state, use moved blocks (since Terraform 1.1) and removed blocks (since 1.7), which put the refactor in code review instead of in someone's shell history. The state rm docs now point to removed blocks as the alternative. Across states, removed with destroy = false in the old configuration plus an import block in the new one does the same job reviewably.
HashiCorp's own answer to coordinating many states is Terraform Stacks, generally available for current HCP Terraform plans since HashiConf in September 2025. Stacks model components and deployments (one per environment or region) in .tfcomponent.hcl and .tfdeploy.hcl files and orchestrate the dependencies between them. It is a reasonable design and it is HCP-only; if you are on OpenTofu or self-hosted state, Terragrunt or a plain pipeline with ordered jobs remains the pattern. My comparison of Crossplane and Terraform covers the other route, where a controller owns state continuously.
Does state still matter when agents write the Terraform?
More than before. An agent generating HCL is bounded by review. An agent holding credentials that can write state, or run force-unlock, is bounded by nothing except the IAM policy you gave it. The guardrails are dull and effective: plans in CI, applies after merge, a short-lived per-environment role for the pipeline, no state write access for anything interactive, versioning on the bucket, and prevent_destroy on resources that hold data. I make the same argument at more length in using LLMs to write infrastructure code safely. None of it is new. It is just no longer optional.
FAQ
Is the DynamoDB table still needed for Terraform S3 state locking?
No. Since Terraform 1.11, use_lockfile = true locks state using an S3 conditional write and DynamoDB locking is deprecated, with removal planned for a future minor version. OpenTofu 1.10+ supports the same option but has not deprecated DynamoDB. Keep both enabled briefly during migration, then remove dynamodb_table.
Does sensitive = true encrypt Terraform state?
No. Sensitive values are still written to state and plan files in plaintext; the flag only hides them in CLI and UI output. Use ephemeral values or write-only arguments to keep secrets out of state, and OpenTofu client-side encryption or HCP Terraform HYOK to encrypt what is left.
How many resources should one Terraform state hold?
There is no official limit, but plan time, lock contention and blast radius all grow with state size. Split per environment and per component that changes independently, and stop when two pieces always change together.
Can I edit a Terraform state file by hand?
You can, but prefer moved, removed and import blocks, which are reviewed and planned. If you must edit, pull a copy with terraform state pull, keep the lineage unchanged, increment the serial and push it back, with bucket versioning as your rollback.
This quarter I would do three things on any estate I inherited: switch every S3 backend to use_lockfile and delete the lock table once all pipelines are on the new config, confirm versioning and a restrictive bucket policy on every state bucket, and move any database password still sitting in state to a write-only argument. If you are hitting a stuck lock right now, the step-by-step is in the Terraform state lock error guide.
Related: Infrastructure as Code in 2026: Two Forks at 1.16 and 1.13, a $6.4B Owner, 912 Public State Files, and One Agent That Ran terraform destroy, It's 2 AM. Do You Know What Your AI Agent Is Doing? and Who Is This Agent? Non-Human Identity in 2026, From the RFCs to the Breaches.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
AI Agent Failures in Production: 5 Incidents, 14 Failure Modes (2026)
What actually goes wrong when AI agents run in production: the Replit, Amazon Kiro, PocketOS and PromptPwnd incidents, the MAST taxonomy of 14 failure modes from 1,642 traces, cost loops, permission mistakes, and the guardrails that stopped them.
12 minOct 20, 2026Cloud Outages 2025-2026: What 7 Official Postmortems Actually Teach
The AWS us-east-1 DynamoDB DNS outage, Azure Front Door, two Cloudflare global failures, Google Cloud Service Control and two 2026 incidents, read from the official postmortems: root causes, durations, and the static stability, cell and dependency-mapping fixes that follow.
14 minOct 20, 2026Ingress NGINX Is Retired: What Replaces It and How to Choose in 2026
Ingress NGINX stopped receiving fixes in March 2026 after seven CVEs in its final seven weeks. What retirement means, why IngressNightmare mattered, where Gateway API stands at v1.6, and how to pick between Envoy Gateway, Cilium, Istio, NGINX Gateway Fabric, Traefik and kgateway.
13 min