# Terraform State Explained: Locking, Backups, and Recovery

> Originally published on [DevToolHub](https://devtoolhub.com/terraform-state-explained/).

Terraform state causes more real incidents than almost anything else in the tool — a stuck lock, a corrupted file, a `state rm` that touched the wrong resource. Most of the anxiety is unnecessary once you know two things most tutorials skip: Terraform backs up state automatically before every change, and it can't be turned off. Here's terraform state explained with the commands that actually matter, verified against HashiCorp's own docs.

## Terraform State Backups: The One You Didn't Know You Had

Per [Terraform's state documentation](https://developer.hashicorp.com/terraform/language/state), Terraform "stores each workspace's state in a local file named `terraform.tfstate`, and a backup of the previous state in `terraform.tfstate.backup`." That backup isn't optional. Per the [state command reference](https://developer.hashicorp.com/terraform/cli/commands/state), every command that modifies state "write[s] backup files," and backups for state modification "can not be disabled" — HashiCorp's own words, because of how sensitive state content is.

Practically: before you panic about a bad `terraform state rm` or `state mv`, check for `terraform.tfstate.backup` first. It's sitting right next to your state file, one command edit behind.

## Terraform State Commands That Actually Matter

Six subcommands live under `terraform state`, and you'll use maybe three of them regularly:

| Command | What it does |
|---|---|
| `terraform state list` | Lists every resource currently tracked in state |
| `terraform state show <resource>` | Shows the full recorded attributes for one resource |
| `terraform state mv <src> <dst>` | Moves a resource to a new address in state, without touching real infrastructure |
| `terraform state rm <resource>` | Removes a resource from state tracking, without destroying it |
| `terraform state pull` | Retrieves and prints the raw remote state |
| `terraform state replace-provider` | Updates which provider a resource is associated with |

`mv` and `rm` are the two people reach for under pressure — renaming a resource in code without triggering a destroy/recreate, or un-tracking something Terraform shouldn't manage anymore. Both are state-only operations: nothing in your real infrastructure changes. That's also exactly why the automatic backup exists — a bad `mv` or `rm` is a state-file mistake, and the backup is how you undo it.

## State Locking: The DynamoDB Pattern Is Deprecated

If you learned Terraform's S3 backend from an older tutorial, it told you to provision a DynamoDB table for state locking. That guidance is now out of date. Per the [S3 backend docs](https://developer.hashicorp.com/terraform/language/backend/s3), DynamoDB-based locking is explicitly deprecated: "Locking can be enabled via S3 or DynamoDB. However, DynamoDB-based locking is deprecated and will be removed in a future minor version."

The current approach is native S3 locking, enabled with one config key:

```hcl
terraform {
  backend "s3" {
    bucket       = "my-terraform-state"
    key          = "prod/terraform.tfstate"
    region       = "us-east-1"
    use_lockfile = true   # native S3 locking — no DynamoDB table needed
  }
}
```

The lock lives alongside your state as a file with a `.tflock` suffix. If you're still running a DynamoDB table purely for Terraform locking, it's no longer required — `use_lockfile = true` replaces the `dynamodb_table` setting, and one less piece of infrastructure to maintain and pay for.

## When a Lock Won't Release: force-unlock

A crashed `apply`, a killed CI job, or a network drop mid-run can leave a lock in place with nothing actually running. Per the [force-unlock reference](https://developer.hashicorp.com/terraform/cli/commands/force-unlock), the command is:

```
terraform force-unlock [options] LOCK_ID
```

The important line in HashiCorp's own docs: "This command does not modify your infrastructure." It only removes the lock record — it doesn't touch state content or real resources, which is why it's safe to run once you've confirmed nothing else is actually mid-apply. One real caveat worth knowing before you reach for it: "Local state files cannot be unlocked by another process" — `force-unlock` is a remote-backend feature. If you're on purely local state, a stuck lock means finding and clearing the lock file yourself, not running this command.

## A Practical Recovery Workflow

Put together, here's the actual sequence for the two most common state incidents:

**"I ran `terraform state rm` on the wrong resource."**
1. Don't run `terraform apply` yet — that's what would actually recreate or destroy something based on the now-wrong state.
2. Check `terraform.tfstate.backup` — it has the pre-`rm` version.
3. Restore the specific resource block from the backup, or use `terraform state push` with a corrected copy if you're on a remote backend.

**"A lock won't release and nothing is actually running."**
1. Confirm no CI job or teammate has an apply genuinely in progress — check your CI dashboard, not just your own terminal.
2. Get the lock ID from the error message Terraform printed when the lock conflict occurred.
3. Run `terraform force-unlock LOCK_ID`. Remember this doesn't work for purely local state.

Neither of these requires touching real infrastructure. That's the point of how Terraform separates state operations from apply operations — it's worth knowing before the 2am incident, not during it.

For the tool that automates a lot of this multi-environment state and backend setup, our [Terragrunt vs Terraform guide](https://devtoolhub.com/terragrunt-vs-terraform/) covers `remote_state` blocks that auto-generate backend config like the one above. If you're deciding between engines entirely, our [OpenTofu vs Terraform guide](https://devtoolhub.com/opentofu-vs-terraform/) and our look at [what's new in OpenTofu 1.13](https://devtoolhub.com/opentofu-1-13-whats-new/) cover the state-encryption feature Terraform doesn't have natively.

## Frequently Asked Questions

**Q: Does Terraform back up state automatically?**
A: Yes. Per HashiCorp's own docs, every state-modifying command writes a backup to `terraform.tfstate.backup`, and this cannot be disabled.

**Q: Do I still need a DynamoDB table for Terraform state locking?**
A: No, not for new setups. DynamoDB-based S3 locking is deprecated. Use `use_lockfile = true` in your S3 backend config for native locking instead.

**Q: Does terraform force-unlock destroy anything?**
A: No. Per HashiCorp's docs, it explicitly does not modify your infrastructure — it only removes the lock record. It also doesn't work on local (non-remote) state files.

**Q: What's the difference between terraform state rm and terraform destroy?**
A: `state rm` only removes a resource from Terraform's tracking — the real infrastructure is untouched. `destroy` actually deletes the real resource. They're easy to confuse in a panic; `state rm` is the safe one.

**Q: Where is Terraform state stored by default?**
A: Locally in `terraform.tfstate` unless a remote backend (S3, HCP Terraform, etc.) is configured. HashiCorp recommends a remote backend for anything beyond solo experimentation.

**Quick Summary:**
- Terraform state modification commands (`state mv`, `state rm`) always write an undeletable backup to `terraform.tfstate.backup` first
- DynamoDB-based S3 state locking is deprecated — use `use_lockfile = true` for native S3 locking instead
- `terraform force-unlock LOCK_ID` removes a stuck lock without touching infrastructure, but doesn't work on local state
- The safe recovery path for a bad state edit is the `.tfstate.backup` file, not `terraform apply`
- `state rm` and `terraform destroy` are not the same thing — only one of them touches real infrastructure

Check `terraform state list` before any bulk state operation — confirming what's actually tracked takes ten seconds and prevents the kind of terraform state mistake that needs a recovery workflow in the first place.

