Quick answer: Terraform state is a JSON document (terraform.tfstate) that records which real-world resources Terraform created and what their attributes were at the last apply. Terraform compares your .tf code against this state, and the state against the real cloud, to work out the minimum set of changes to make. Lose or corrupt the state and Terraform forgets what it owns; that is why state must be stored remotely, locked, encrypted and never edited by hand.
Almost every scary Terraform story β the accidental recreate, the “resource already exists” error, the two engineers who overwrote each other’s changes β is a state story. This guide explains what the state file contains, how Terraform uses it during plan and apply, local versus remote storage, the terraform state commands you will actually use, and the habits that keep state safe.
What the state file is and why Terraform needs it
Your .tf files describe the desired infrastructure. The cloud API holds the actual infrastructure. State is the bridge between them: a mapping from each resource address in your code (aws_instance.web) to the real object’s ID (i-0abc123) and its last-known attributes.
Without that mapping Terraform could not answer basic questions. Which of the 300 EC2 instances in this account is “my” aws_instance.web? Did I create this security group, or did someone else? Should changing instance_type update the existing machine or build a new one? State answers all of these.
State also stores metadata that is not available from the API: resource dependencies (so Terraform destroys things in the right order), provider configuration for each resource, and output values. For the basics of how resources are declared before they ever reach state, see Creating and Managing Infrastructure Resources in Terraform.
Inside a state file
You should never edit state manually, but reading it is instructive. A trimmed example after creating one S3 bucket:
{
"version": 4,
"terraform_version": "1.9.5",
"serial": 7,
"lineage": "3f2b9c1e-6a7d-4f1a-9c0b-5e8d2a1b7c44",
"outputs": {
"bucket_arn": {
"value": "arn:aws:s3:::tkh-demo-logs",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_s3_bucket",
"name": "logs",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"arn": "arn:aws:s3:::tkh-demo-logs",
"bucket": "tkh-demo-logs",
"id": "tkh-demo-logs",
"region": "ap-south-1",
"tags": { "ManagedBy": "terraform" }
},
"dependencies": []
}
]
}
]
}
Key fields:
serialincrements on every write; backends use it to detect stale writes.lineageis a unique ID for this state’s history. Terraform refuses to overwrite state with a different lineage, which protects you from pointing two projects at one file.modeismanagedfor resources anddatafor data sources.attributesholds every value the provider returned, including sensitive ones such as database passwords, in plain text.
How Terraform uses state during plan and apply
Every terraform plan runs the same loop:
- Read state from the configured backend.
- Refresh: for each resource in state, ask the provider for its current real attributes and update the in-memory copy. This is how Terraform notices that someone changed a tag in the console.
- Diff: compare the refreshed state with the desired configuration and compute create, update, replace or destroy actions.
- On apply, execute those actions and write the new state back, bumping
serial.
This is why a resource deleted by hand in the console shows up in the next plan as “will be created”: state said it existed, refresh found it gone, configuration still wants it. The ordering of those actions comes from the dependency graph, which Terraform builds partly from state; Understanding Resource Dependencies and Ordering in Terraform covers that in depth.
Local vs. remote state
| Local state (default) | Remote state (backend) | |
|---|---|---|
| Where it lives | terraform.tfstate in your working directory |
S3, Azure Blob, GCS, HCP Terraform, Consul, PostgreSQL and others |
| Team access | One laptop only | Everyone and CI share one source of truth |
| Locking | None across machines | Supported by most backends (DynamoDB or native S3 locking, blob leases, HCP) |
| Encryption and backup | Whatever your disk has | Server-side encryption, versioning, access logs |
| Good for | Learning and throwaway experiments | Anything shared or long-lived |
Switching is a few lines. Here is a production-grade S3 backend for an AWS project in Mumbai:
terraform {
backend "s3" {
bucket = "tkh-terraform-state"
key = "payments/prod/terraform.tfstate"
region = "ap-south-1"
encrypt = true
use_lockfile = true # native S3 locking, Terraform 1.10+
}
}
Run terraform init after adding the block and Terraform offers to migrate the existing local state into the bucket. Enable bucket versioning so every previous state is recoverable.
The terraform state commands you will actually use
Terraform ships a family of subcommands for inspecting and surgically adjusting state. They are safer than a text editor because they validate the result and (with remote backends) take a lock.
# See everything Terraform manages
terraform state list
# Inspect one resource's recorded attributes
terraform state show aws_s3_bucket.logs
# Rename a resource in state after refactoring code (no destroy/create)
terraform state mv aws_instance.web aws_instance.app
# Stop managing a resource without deleting it in the cloud
terraform state rm aws_iam_user.legacy
# Bring an existing resource under management
terraform import aws_s3_bucket.assets tkh-prod-assets
# Download a copy of the current state as JSON
terraform state pull > backup.tfstate
Since Terraform 1.1 you can replace most state mv calls with a declarative moved block in code, and since 1.7 a removed block replaces state rm. Both are reviewable in a pull request, which ad-hoc CLI commands are not. For bringing resources in, Terraform 1.5+ also offers import blocks that generate configuration for you.
Six state mistakes that cause outages
- Committing
terraform.tfstateto Git. State contains secrets in plain text and merge conflicts corrupt it. Add*.tfstate*to.gitignoreand use a remote backend. - Editing the JSON by hand. A typo in
serialor a missing comma makes the file unreadable. Useterraform statecommands ormovedandremovedblocks. - Two root modules sharing one state key. Each applies will try to destroy the other’s resources. One backend
keyper root module, always. - Running apply without a lock. Two concurrent applies both write state; the slower one wins and the faster one’s resources are orphaned. Enable locking on your backend.
- Treating state as the only truth. Drift happens. Run
terraform planregularly in CI and alert on unexpected changes. - No backups or versioning. Turn on S3 bucket versioning or the equivalent, so a bad write can be rolled back with
terraform state push.
Frequently asked questions
Does the state file contain secrets?
Yes. Database passwords, private keys and tokens returned by providers are stored in plain text, even when marked sensitive (that only hides them in CLI output). Encrypt the backend and restrict read access as tightly as you would for the secrets themselves.
What is the difference between terraform refresh and plan?
plan refreshes state in memory and shows the diff without saving. The standalone refresh command writes the refreshed state immediately and is deprecated; use terraform apply -refresh-only when you want to accept drift into state.
Can I recover if the state file is deleted?
If you have backend versioning, restore the previous object. If not, you must re-import every resource with terraform import. This is slow and error-prone, which is why versioning is non-negotiable.
What is a state backup file?
With local state, every write produces terraform.tfstate.backup containing the previous version. Remote backends do not create this file; they rely on the storage service’s own versioning.
Key takeaways
- State maps your code’s resource addresses to real cloud objects; Terraform cannot plan without it.
- It is JSON, it contains secrets, and it must be stored remotely, encrypted, versioned and locked.
- Use
terraform state list / show / mv / rm, ormoved,removedandimportblocks, instead of editing the file. - One state per root module, backups turned on, and regular plans to catch drift.
Ready to run Terraform the way production teams do, with remote state, locking and CI pipelines? Our DevOps course covers Terraform end to end with live cloud labs, mentor support and placement assistance. You can also watch the hands-on sessions on our YouTube channel.


