Fixing the Terraform "Saved plan is stale" Error in CI/CD Pipelines

intermediate🏗️ Terraform2026-06-15| Terraform CLI, CI/CD platforms (GitHub Actions, GitLab CI), and Remote Backends like AWS S3 with DynamoDB or Terraform Cloud.

Error Message

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.
#terraform#devops#iac#ci-cd#troubleshooting

The ScenarioYou have just finished a careful review of terraform plan -out=tfplan. The output looks perfect, so you trigger terraform apply "tfplan". But instead of seeing resources spin up, your terminal spits out a blunt error message. Your deployment halts before it even begins.

The Exact Error```

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.


In high-velocity teams, the infrastructure state is a moving target. By the time you tried to apply your plan, a teammate or an automated script likely modified the state file. This mismatch renders your saved plan instantly obsolete.
## Why This HappensTerraform relies on a state file (`terraform.tfstate`) as its absolute source of truth. Every state file includes a `serial` number—an integer that starts at 0 and increments by 1 every single time the state is modified.
When you create a plan file using the `-out` flag, Terraform stamps the current state's `serial` and `lineage` into that file. For instance, if your state is at serial 45, your `tfplan` expects 45.

If a coworker runs a `terraform apply` or even a `terraform state rm` while you are reviewing your code, the serial in the backend jumps to 46. When you eventually run your apply, Terraform detects that 45 is less than 46 and stops. This safety check prevents you from applying changes based on old data, which could otherwise delete newly created resources or corrupt your environment.
Typical triggers include:
- A teammate merged a quick hotfix that updated the state.- A concurrent CI/CD job finished a deployment on a different branch.- You manually tweaked the state using `terraform import` between your plan and apply steps.## Quick Fix: The "Fresh Start" MethodYou cannot "refresh" or force a stale plan to work. The only path forward is to sync with the current state and generate a new plan.
- **Pull the latest state:** Terraform usually does this during the plan phase, but you can run `terraform refresh` to be certain.- **Generate a new plan:**```
terraform plan -out=tfplan
```- **Apply the new plan immediately:**```
terraform apply "tfplan"
```If you hit the same error again immediately, your infrastructure is changing faster than your manual workflow can keep up. This suggests a bottleneck in your team's coordination or a flaw in your pipeline's concurrency logic.
## Permanent Fix: Workflow & ArchitectureFrequent stale plan errors are a symptom of a loose deployment process. Use these three strategies to lock down your workflow.
### 1. Mandatory State LockingIf your backend doesn't support locking, you are inviting race conditions. For AWS S3 backends, a DynamoDB table is non-negotiable for state locking. It ensures that only one person or process can write to the state at a time.

terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-lock-table" # Essential for team safety } }


### 2. Tighten the Plan-to-Apply WindowMany pipelines generate a plan and then wait hours for a manual approval. The longer that window stays open, the higher the chance of a collision. To minimize risk:
- Use **concurrency limits** in GitHub Actions (`concurrency: group_name`) to ensure only one pipeline runs per environment.- Auto-apply changes in lower environments like `dev` or `staging` to keep the state moving quickly.- Set a 30-minute expiration on manual approval steps.### 3. Ban Manual State SurgeryDiscourage running commands like `terraform state rm` or `terraform import` from local machines. These operations increment the serial number instantly. If manual changes are required, coordinate a "maintenance window" where pipelines are temporarily paused.
## Verification StepsUse these steps to confirm your state is back in sync:
- **Inspect the current serial:** Pull the state and check the versioning manually.```
terraform state pull | jq '.serial'
```- **Run a dry-run:** Execute a fresh `terraform plan`. If it reports "No changes," another process already applied the changes you were planning to make.- **Validate the lock:** Try running two `plan` commands in separate terminals. If the second one fails with a "Lock acquired by..." error, your safety net is working.## Final ThoughtsThe "Saved plan is stale" error isn't a bug—it's Terraform acting as a guardrail. It protects your production environment from "last-write-wins" scenarios that could inadvertently wipe out hours of work. While regenerating the plan is the quick fix, implementing strict state locking and pipeline concurrency is the only way to eliminate the problem for good.

Related Error Notes