The "Saved plan is stale" error occurs when Terraform detects that the state file has changed after you created a plan but before applying it. This typically happens in CI/CD pipelines or when multiple operations modify the state concurrently.
Terraform creates a plan by comparing your configuration against the current state file. The plan file is essentially a record of the changes Terraform intends to make. However, if the state file is modified by another operation after the plan is created but before it's applied, Terraform considers the plan "stale" and refuses to apply it. This safety mechanism prevents inconsistent infrastructure changes. Terraform detects staleness by comparing the state's serial number (a counter that increments each time the state is modified) between when the plan was created and when the apply operation begins. If the serials don't match, the state has changed and the plan is no longer valid.
The simplest solution is to regenerate the plan:
# Remove the stale plan file
rm tfplan
# Generate a new plan
terraform plan -out=tfplan
# Apply the new plan
terraform apply tfplanThis ensures the plan is created with the current state and captures any state changes that occurred between the original plan and this moment.
In multi-stage pipelines (TEST, QA, PROD), don't save and reuse plan files across stages. Instead, create a fresh plan in each stage:
# Bad approach (prone to stale plan errors)
stages:
plan:
- terraform plan -out=tfplan
apply:
- terraform apply tfplan # May fail if state changed
# Good approach (create fresh plan per stage)
stages:
plan:
- terraform plan
apply:
- terraform plan -out=tfplan
- terraform apply tfplanThis ensures each apply uses a plan created from the current state at execution time.
If you want to understand what changed that made the plan stale, compare the old plan with a new one:
# Save the original plan as text
terraform show -no-color tfplan.old > tfplan.old.txt
# Create a new plan
terraform plan -no-color -out=tfplan.new > tfplan.new.txt
# Compare the differences
diff tfplan.old.txt tfplan.new.txtThis shows you exactly what changed in the state between the two plan generations. Look for state serial increments and data source changes.
If the state was modified due to out-of-band infrastructure changes, use refresh-only mode to sync your state:
# Create a refresh-only plan (updates state without modifying resources)
terraform plan -refresh-only -out=tfplan.refresh
# Review the changes
terraform show tfplan.refresh
# Apply the refresh-only plan
terraform apply tfplan.refresh
# Now create your normal plan
terraform plan -out=tfplan
terraform apply tfplanThe -refresh-only flag is safer than the deprecated terraform refresh command because it lets you review changes before applying them.
If you want faster planning during development and trust your state is current, you can skip the refresh step:
# Plan without refreshing state (faster, but skips out-of-band change detection)
terraform plan -refresh=false -out=tfplan
# Apply directly
terraform apply tfplanNote: This is only safe if you're confident no external changes have been made to your infrastructure. Don't use this in CI/CD pipelines handling production infrastructure.
State Serial Increments: Each time the state is modified, Terraform increments the state's serial number. This is how Terraform detects whether the state has changed. Even if the actual resource configuration hasn't changed, data sources or other state-modifying operations will increment this serial, making previously-created plans invalid.
Data Source Side Effects: Some data sources (like random_id or uuid) generate new values on every application. Even if you don't explicitly ask for new values, they may be re-generated during plan/apply cycles, modifying the state serial.
HCP Terraform Behavior: If you're using HCP Terraform (formerly Terraform Cloud), it automatically discards stale plans if another run is applied before your saved plan run is confirmed. This is by design to prevent exactly this problem.
Rootless Terraform: In rootless Terraform deployments with managed state, the state timestamps are sometimes used for staleness detection rather than serial numbers alone. Ensure your state backend is properly synchronized across operations.
Lock Files: State lock files are not the same as plan staleness. If you see "Error acquiring state lock" instead, that's a different issue related to state locking timeouts.
Error: Error installing helm release: cannot re-use a name that is still in use
How to fix "release name in use" error in Terraform with Helm
Error: Error creating GKE Cluster: BadRequest
BadRequest error creating GKE cluster in Terraform
Error: External program failed to produce valid JSON
External program failed to produce valid JSON
Error: Unsupported argument in child module call
How to fix "Unsupported argument in child module call" in Terraform
Error: network is unreachable
How to fix "network is unreachable" in Terraform