Error: Saved plan is stale
Fix Terraform "Saved plan is stale": run terraform plan -out again, then apply that fresh plan. Use state locking so state cannot move between plan and apply.
Error: Saved plan is stale
Incident Overview
TerraformTerraform
Fix Terraform "Saved plan is stale": run terraform plan -out again, then apply that fresh plan. Use state locking so state cannot move between plan and apply.
medium fix·5 min read
By Vishvesh Patel·DevOps Engineer·Updated 21 September 2026
Error: Saved plan is stale The given plan file can no longer be applied because the state was changed by another operation after the plan was created. Error: Saved plan does not match configuration
Do this first3 steps
Run these in order. Each one tells you what its output means before you change anything.
• Check whether the state really moved
The serial increments on every state write. Comparing it before and after tells you whether someone else applied in between, which is the usual cause rather than anything wrong with your plan.
• See who wrote to state most recently
With a versioned S3 backend the object history shows each write and when. A write between your plan and your apply confirms a concurrent operation.
• Re-plan and apply in one operation
The gap between the two commands is the window. Narrowing it, or eliminating manual approval in between, removes most occurrences. Do not skip the re-plan and apply the old file.
A saved plan is a promise about a specific state. Terraform records the state serial when the plan is created, and refuses to apply if that serial has moved, because the plan's assumptions may no longer hold.
This is a safety feature. Applying a stale plan could destroy something created since, or fail halfway through on a resource that already changed.
terraform state pull | python3 -c "import sys,json; d=json.load(sys.stdin); print('serial', d['serial'], 'lineage', d['lineage'])"Confirm the state moved
The serial increments on every write. If it is higher than when you planned, something applied in between.
With a versioned S3 backend you can see exactly when:
terraform state pull | python3 -c "import sys,json; d=json.load(sys.stdin); print('serial', d['serial'])"What changed it
Another apply. A colleague, or a second pipeline run. The most common cause by a wide margin.
A pipeline with manual approval. Plan runs, waits for a human, and someone else merges and deploys in the meantime. This is the classic CI shape that produces this error routinely.
terraform state commands. state rm, state mv and import all write state and bump the serial.
A refresh. terraform refresh, or terraform plan run with -refresh=true in another shell, writes drift into state.
The fix
Re-plan and apply together:
Do not apply the old plan file. Terraform is refusing for a reason; there is no -force and adding one would be a mistake. The correct response is always a fresh plan.
terraform plan -out=tfplan terraform apply tfplan
Structuring a pipeline so it stops
The safest shape keeps plan and apply in one job:
resource_group: production is the important line. It serialises jobs so two pipelines cannot apply concurrently, which is what produces most of these.
cancel-in-progress: false matters. Cancelling an apply midway leaves a lock and possibly partial changes.
# GitLab CI
apply:
stage: deploy
script:
- terraform init -lockfile=readonly
- terraform plan -out=tfplan
- terraform apply -auto-approve tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
resource_group: productionIf you need review between plan and apply
Keeping the plan artifact is legitimate; the risk is the gap. Reduce it:
An empty diff means the fresh plan is equivalent to what was reviewed, so applying it is safe. This gets you review and freshness together.
terraform show -json tfplan > reviewed.json terraform plan -out=tfplan2 terraform show -json tfplan2 > current.json diff <(jq -S '.resource_changes' reviewed.json) <(jq -S '.resource_changes' current.json)
Confirm locking is actually enabled
use_lockfile is the S3 native locking introduced in Terraform 1.10, replacing the DynamoDB table. Older configurations still use dynamodb_table and that continues to work.
Without locking, two applies can interleave, which produces worse outcomes than a stale plan: state written by one overwrites the other, and resources become orphaned.
Only when you have confirmed no operation is running. A force-unlock on a live apply is how two writers happen.
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "prod/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
}
}"Saved plan does not match configuration"
A related but different error: the plan file was made from different configuration than the one now in the directory. A git pull or a branch switch between plan and apply causes it. Same fix, a fresh plan.
A checklist
Frequently Asked Questions
Why can I not force Terraform to apply a stale plan?
Because the plan is a precise description of the changes to make against a specific state, and if that state has moved, the plan's assumptions may no longer hold. Applying it could delete a resource created since, or fail partway through leaving things half-changed. There is deliberately no override flag. The correct response is always to re-plan, which takes seconds and produces a plan that matches reality.
What actually changes the state serial?
Any write. A completed terraform apply is the obvious one, but so are terraform state rm, state mv and import, and a terraform refresh or a plan run with refresh enabled that detects drift. In a team, the most common cause is simply another person or another pipeline run applying between your plan and your apply, which is why serialising applies matters more than anything else you can do about this.
How do I keep manual approval without hitting this?
Re-plan after the approval rather than applying the stored file, and diff the new plan against the reviewed one to confirm nothing meaningful changed. terraform show -json on both plans, compared with jq -S '.resource_changes', gives a reliable equality check. An empty diff means the fresh plan is equivalent to what a human approved, so you get both review and freshness. Keeping the approval window short also helps considerably.
Does state locking prevent this error?
Not entirely, and it prevents much worse things. Locking stops two applies writing state concurrently, so the serial only moves through completed operations rather than interleaved ones. You can still plan, wait, and have someone else apply successfully in between, which leaves your plan stale. What locking prevents is the scenario where two applies both write state and one silently overwrites the other, orphaning resources. Always have it on.
What is the difference between a stale plan and one that does not match the configuration?
A stale plan means the state changed after the plan was created. A plan that does not match the configuration means the .tf files in the working directory are different from those the plan was generated against, typically after a git pull or a branch switch. Both are refused for the same reason, that the plan no longer describes what would happen, and both are resolved by generating a fresh plan from the current configuration and state.
Reference and practice
Learn the underlying concept
Other Terraform errors
Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.
Get notified about new tutorials and features.
• Roadmaps
• Interview Prep
• Certifications
• Company
© 2026 DevOpsLesson. All rights reserved.
Need to practice this scenario?
Simulate this error condition and practice recovery commands safely in our in-browser terminal.