Refresh & -refresh-only
Terraform’s state file is its memory of the world, but the real infrastructure can change behind its back — someone resizes an instance in the console, an autoscaler swaps a node, or a tag gets edited by a compliance script. Refreshing is how Terraform reads the current attributes of every managed resource from its provider and updates state to match, so the next plan compares against reality rather than a stale snapshot. Knowing how to control refresh — and how to absorb drift with a -refresh-only plan — is essential for keeping state honest without accidentally clobbering live changes.
How refresh works in a normal plan
By default, every terraform plan and terraform apply begins with an implicit refresh. Terraform calls each resource’s ReadResource API, compares the returned attributes to what’s recorded in state, and uses the refreshed values as the baseline for computing changes. This is why a plan can surface a diff even when your configuration hasn’t changed: the remote object drifted.
This refresh is in-memory during plan. The updated values are written to state only when you apply (or when the plan is otherwise persisted). So a read-only plan shows you drift but does not mutate the on-disk state until you act on it.
The standalone
terraform refreshcommand still exists but is deprecated. It updates state silently with no review step. Preferterraform apply -refresh-only, which shows you exactly what changed before you commit it. OpenTofu follows the same guidance.
Detecting and absorbing drift with -refresh-only
A -refresh-only plan answers one question: how does my state differ from the real world? It performs the refresh, reports any drift, and proposes only to update state — it never adds, changes, or destroys infrastructure.
Suppose this is your configuration:
resource "aws_instance" "web" {
ami = "ami-0c0b74d29acd0cd97"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
Someone manually adds a Team = "platform" tag in the AWS console. Run a refresh-only plan to see it:
terraform plan -refresh-only
Output:
aws_instance.web: Refreshing state... [id=i-0abc123def4567890]
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
# aws_instance.web has changed
~ resource "aws_instance" "web" {
id = "i-0abc123def4567890"
~ tags = {
+ "Team" = "platform"
# (1 unchanged element hidden)
}
# (30 unchanged attributes hidden)
}
This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.
To record the drift into state — adopting the live values as the new baseline — apply the refresh-only plan:
terraform apply -refresh-only
After approval, state now knows about the Team tag. A subsequent terraform plan will then show that your configuration wants to remove it (since it isn’t declared), letting you decide deliberately: either add the tag to your HCL or let Terraform remove it.
This two-step flow — absorb drift, then reconcile config — is the safe way to handle out-of-band changes. Compare it to a normal apply, which would immediately try to revert the drift to match your configuration.
Skipping refresh with -refresh=false
Refresh has a cost: it makes a read API call for every managed resource. On a large workspace with thousands of resources, that can take minutes and hit provider rate limits. When you’re confident state is current — for example, applying a plan you just generated seconds ago — you can skip it:
terraform plan -refresh=false
terraform apply -refresh=false
With -refresh=false, Terraform trusts the values already in state and computes the diff against them. This is faster but risky: if the real world has drifted, the plan will be based on stale data and may produce an incorrect or surprising result.
A common, safe pattern is to refresh once when generating a saved plan, then apply it without re-refreshing:
terraform plan -out=tfplan
terraform apply -refresh=false tfplan
Saved plan files already encode the refreshed state, so
terraform apply tfplandoes not re-refresh by default. Passing-refresh=falseto the plan step is what you’d skip to save time — only do it when nothing external touches your resources.
Option reference
| Flag / command | Effect | Mutates state? |
|---|---|---|
| (default) | Refresh in-memory before planning the diff | Only on apply |
-refresh-only | Plan/apply only state reconciliation, no resource changes | On apply |
-refresh=false | Skip the refresh, use existing state as baseline | No |
terraform refresh | Deprecated; refreshes and writes state with no review | Yes, immediately |
Best Practices
- Use
terraform apply -refresh-onlyinstead of the deprecatedterraform refreshso drift is reviewed before being recorded. - Treat the “Objects have changed outside of Terraform” notice as a signal to investigate why the drift happened, not just to absorb it.
- After a refresh-only apply, run a normal
planto consciously decide whether to keep or revert each drifted value. - Reserve
-refresh=falsefor large workspaces or tight CI loops where you can guarantee no out-of-band changes. - Pair
plan -out=tfplanwithapply tfplanso the applied changes match exactly what was reviewed, refresh included. - Run periodic refresh-only plans (e.g. nightly in CI) to detect configuration drift early across long-lived environments.