Skip to main content
DevOps Insightsguides & answers

Terraform · troubleshooting

Error: Saved plan is stale

A saved plan records the state it was worked out against, and Terraform refuses to apply it once that state has changed. Anything that writes the state in between counts — another apply, a retried job, even terraform state rm on a resource the plan never touches — because the plan's changes were calculated for a state that no longer exists. There is no flag to force it, and you should not want one: plan again.

Reproduced on Terraform 1.14, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 9 sections

Before you start

  • The plan file and a terminal or CI job set up exactly like the one that made it: same backend settings, same workspace and same Terraform version.
  • Read access to the state, to run terraform state pull, plus unzip and jq if you want to compare the plan's lineage and serial with the current state.
  • Your CI history for this state: which pipelines planned or applied since the plan was made, and whether the failing job is a retry.
  • Permission to run a fresh terraform plan and have it reviewed again before anything is applied.

Run this first

step 1 of 2
unzip -p tfplan tfstate | jq '{lineage, serial}'; terraform state pull | jq '{lineage, serial}'

Look for Different lineage: another state entirely. Same lineage, higher current serial: written since the plan.

The plan file carries the lineage and serial of the state it was made from; state pull shows the current one. Neither command changes anything, unlike re-running the apply to see what it says.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Which error does terraform apply print?

Cause space

3 of 3 still possible

  • Something wrote the state after the plan was madeCommon
  • The same plan file was applied twiceCommon
  • The plan is being applied to a different stateOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    unzip -p tfplan tfstate | jq '{lineage, serial}'; terraform state pull | jq '{lineage, serial}'

    The plan file carries the lineage and serial of the state it was made from; state pull shows the current one. Neither command changes anything, unlike re-running the apply to see what it says.

    Look for Different lineage: another state entirely. Same lineage, higher current serial: written since the plan.

  2. Step 2

    terraform workspace show

    A different lineage almost always means a different workspace or backend. Run it in the job that made the plan and in the one applying it.

    Look for The same workspace name in both, and no TF_WORKSPACE set in only one of them

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

Something wrote the state after the plan was made

Common

Another pipeline, a teammate or a scheduled job applied to the same state between your plan and your apply. The usual shape is a pipeline that plans on each pull request and applies on merge: two pull requests plan against the same state, the first merge applies, and the second plan is stale. State commands count too — terraform state rm, mv and import write a new serial. State locking does not stop this. HashiCorp's documentation describes locking on operations that could write state, and here the operations ran one after another, so there was never a conflict for a lock to catch — reproduced with the local backend, which locks.

Confirm

unzip -p tfplan tfstate | jq '{lineage, serial}'; terraform state pull | jq '{lineage, serial}'

The same lineage in both, and a higher serial from state pull: the state has been written since the plan. The plan file's layout is internal to Terraform rather than a documented interface, so use this to diagnose, not in scripts.

Fix

  • Run plan again and review the new plan. The old one was calculated against a state that is no longer true, which is exactly why Terraform will not apply it.
  • Apply one plan at a time per state: a concurrency group or queue per workspace in your CI, so a second pipeline waits instead of planning against a state that is about to change.
terraform plan -out=tfplan && terraform apply tfplan

Planning and applying in one job leaves no gap for another writer. Where the plan must be approved first, keep that gap short and serialise applies.

The same plan file was applied twice

Common

A plan file is single-use. Once it has been applied, the state has a new serial and the file is stale. CI is where this happens: someone re-runs a failed apply job, and it picks up the plan artifact from the original run. Reproduced: an apply that failed partway through still wrote state — the serial went from 1 to 4 — so retrying that same plan file was refused. That is the right outcome. Part of the plan had already happened, and the rest has to be worked out again from where things now stand.

Confirm

unzip -p tfplan tfstate | jq .serial; terraform state pull | jq .serial

A current serial higher than the plan's, with the earlier apply job in the same pipeline as the obvious writer

Fix

  • Re-plan from the current state and review what is left to do — after a partial failure, that is usually less than the original plan.
  • Make a retry run plan and apply together, rather than re-running only the apply step against the old artifact.

The plan is being applied to a different state

Occasional

"Saved plan does not match the given state" means the lineage differs: the apply step is looking at another state from the one the plan was made from. Reproduced by applying a plan in a different workspace. In CI that is usually the plan and apply jobs initialising differently — a different backend key, different -backend-config values, or a TF_WORKSPACE that is set in one job and not the other. One exception from Terraform's source: a plan made against an empty state carries no lineage, so applying it after some other run created the state reports "stale" rather than "does not match".

Confirm

terraform workspace show; unzip -p tfplan tfstate | jq -r .lineage; terraform state pull | jq -r .lineage

Different lineages, and a workspace or backend setting that differs between the plan job and the apply job

Fix

  • Initialise both jobs with identical backend configuration and select the same workspace in each.
  • HashiCorp's automation guide says to archive the whole working directory after plan, including .terraform, and extract it at the same absolute path before apply. Following that removes most ways the two jobs can disagree.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

In plain terms

When you save a plan with terraform plan -out, Terraform also saves a copy of the state it planned against. When you apply that file, it first checks that the state has not been written since. If another apply, a retried job or a state command changed the state in between, Terraform refuses the old plan, and the fix is to plan again.

A plan that went stale, reproduced

Reproduced with Terraform 1.14.0, two terraform_data resources and the local backend. terraform plan -out=tfplan saved a change to terraform_data.app while the state was at serial 1. A second terraform apply then changed only terraform_data.other, which took the state to serial 3. Then the saved plan was applied. The output contained no user, host or path details.

Command

terraform apply tfplan

Output

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.

How to read it

  • Saved plan is stale means the lineage matched and the serial did not: this is the right state, but it has been written since the plan was made.
  • The other apply never touched terraform_data.app. The check compares the serial of the whole state, so any write makes every older plan stale.
  • Nothing was applied. The command exited 1, and the state file was byte for byte the same afterwards.
  • unzip -p tfplan tfstate | jq .serial printed 1 and terraform state pull | jq .serial printed 3, which is how you confirm the cause.
  • Applying the same file in another workspace instead gave Saved plan does not match the given state: a different lineage, so a different state.

Words used on this page

Saved plan
The file written by terraform plan -out=tfplan. It holds the planned changes plus copies of the configuration, the dependency lock file and the state it was planned from. terraform apply tfplan carries it out without asking for confirmation.
Lineage
An ID a state receives when it is first created. Every later version of that state keeps it, so a different lineage means a different state altogether.
Serial
A number in the state that goes up whenever Terraform writes a new version of it. One apply can raise it more than once.
Stale
Terraform's word for a saved plan whose state has the same lineage but a different serial: the right state, written to since.
Workspace
A separately stored state for the same configuration, such as default or staging, chosen with terraform workspace select or the TF_WORKSPACE variable.
State lock
A lock Terraform takes during any operation that could write state. It stops two writes at once, but not a write that lands between your plan and your apply.
terraform state pull
A command that prints the current state as JSON, including its lineage and serial, without changing it.
What is actually happening

terraform plan -out saves more than a list of changes. Reproduced on Terraform 1.14, the plan file holds a snapshot of the state it was computed from, a copy of the configuration and the dependency lock file. Applying it uses that copy: a .tf file edited after the plan was saved had no effect on the apply.

Before it applies anything, Terraform compares the snapshot with the current state on two values. Lineage identifies a state across its whole life; the serial goes up each time a new version of it is written. In Terraform 1.14's source, a different lineage produces "Saved plan does not match the given state" — the plan came from some other state entirely — and a different serial produces "Saved plan is stale": the same state, written to since.

Because the check is on the serial, it is about the state as a whole, not the resources in your plan. Reproduced: another apply that changed something, a second apply of the same plan file, an apply that failed partway, and a terraform state rm of a resource the plan did not mention all made the plan stale. Someone else running terraform plan, a no-op apply and a refresh-only apply that found no drift did not, because none of them wrote a new state.

HashiCorp's automation guide states the rule from the other side: when a plan is applied, any other existing plans produced against the same state are invalidated, because they must be recomputed relative to the new state. The error is that rule being enforced — which matters because a saved plan applies without asking for confirmation.

How to tell this is your problem
WhereWhat you see
terraform apply <planfile>Error: Saved plan is stale
terraform apply <planfile>, the detailThe given plan file can no longer be applied because the state was changed by another operation after the plan was created.
terraform apply <planfile>Error: Saved plan does not match the given state
CIThe apply job fails on a retry, or when it runs long after its plan job and another pipeline has applied in between

How to know it is actually fixed

  • A fresh plan saved and applied straight away completes.
  • At the moment of apply, the plan file's lineage and serial match terraform state pull.
  • A re-run of the apply job in CI produces a new plan rather than reusing the old artifact.

Stopping it happening again

  • Treat a saved plan as single-use. After it is applied, or after anything else writes the state, it is spent.
  • Serialise applies per state with a concurrency group or queue. State locking covers one operation at a time, not the gap between a plan and its apply.
  • Make retries re-plan. Re-running only the apply step against an old plan artifact is the commonest way to meet this error.
  • Give plan and apply jobs the same backend configuration and workspace, and carry the whole working directory between them as HashiCorp's automation guide describes.
  • Store plan artifacts as carefully as state. HashiCorp's guide says that if anything in the archived working directory is sensitive, the archive containing the plan file should be protected accordingly.

Security considerations

  • A plan file holds a full copy of the state it was planned from, and HashiCorp's plan documentation says sensitive data in a plan is saved in cleartext. Store plan artifacts with the same access control as state, keep them short-lived and delete them once applied or stale.
  • A saved plan applies without a confirmation prompt. Whoever can swap the artifact between review and apply decides what runs, so make plan artifacts read-only to everything except the job that wrote them, and apply only the file that was reviewed.
  • Do not force an old plan through by pushing its state snapshot with terraform state push -force or by editing the serial. HashiCorp's lineage and serial checks exist to stop exactly that, and overriding them overwrites the newer state, losing the record of whatever was written since.
  • A lineage error can mean the apply job is pointed at the wrong state. Check backend settings and workspace before anything else; HashiCorp recommends TF_WORKSPACE only for non-interactive use, because a forgotten variable can apply changes to the wrong state.
  • After a partly failed apply, read the new plan rather than approving it on trust. Reproduced on Terraform 1.14.0: the resource that failed was left in state as tainted, and the next plan said it is tainted, so must be replaced.

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it