Skip to main content
RunBook Academy

TerraformII · Terraform ArchitectureProduction Terraform

Refresh and Plan: How They Interact

Intermediate⏱ ~12 minbash

What you'll learn

  • Explain what refresh does and when it runs by default in Terraform 1.9.x
  • Distinguish refresh from re-plan
  • Apply -refresh-only to update state after out-of-band changes
  • Recognise how -target and -replace interact with refresh
  • Identify the production failure modes of refresh behaviour

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

Refresh is the operation that brings Terraform’s state into agreement with the real world. It is read-only; it does not change infrastructure and does not change the configuration. But it does change the state file, and that subtle property is the source of most “the plan is lying to me” tickets in production. This lesson covers what refresh does, when it runs in 1.9.x, and how the -refresh-only mode and -replace flag interact with the plan.

What refresh does

For each resource tracked in state, refresh asks the provider to read the current real-world attributes and write them into state. Refresh does not propose changes to the configuration; it proposes changes to the state file.

State (last refresh)        Real world (now)
        |                           |
        |  provider ReadResource    |
        |  <----------------------- |
        |                           |
        v                           |
   State (refreshed)  <-------------+

If state and reality agree, refresh is a no-op. If state and reality disagree (drift), refresh updates state with reality’s view. The next plan sees the updated state and computes the diff against the new state, not the old.

When refresh runs in Terraform 1.9.x

The history of refresh behaviour is worth knowing because old blog posts and Stack Overflow answers describe the 0.x behaviour:

  • Terraform 0.x — 0.15.3: refresh was implicit during plan and apply. There was no way to disable it.
  • Terraform 0.15.4 (2021): refresh during plan became optional via -refresh=false. Refresh during apply was also optional.
  • Terraform 1.0 — 1.2: refresh defaulted to enabled during plan and apply. The terraform refresh standalone command was deprecated.
  • Terraform 1.3: the -refresh-only mode was added to terraform plan and terraform apply. The standalone terraform refresh command was removed.
  • Terraform 1.9.x (this course): refresh defaults to enabled during plan. Refresh during apply was changed in 1.3 to default to disabled during apply unless the state has changed since the plan was created.

The 1.3 change is subtle but operationally significant. Before 1.3, every apply refreshed state by default. In 1.3+, apply refreshes state only if the state has changed since the plan was created (which catches the common case of a separate terraform refresh between plan and apply) or if the plan was not saved.

Refresh vs re-plan

Two related but distinct operations:

  • Refresh updates the state file with the current real-world attributes. It does not look at the configuration. It does not compute a diff.
  • Re-plan re-reads the configuration, asks the provider for the diff for each resource, and produces a new plan.
# Refresh only: state updated, no plan produced
terraform apply -refresh-only

# Re-plan: state refreshed + new plan computed
terraform plan

In Terraform 1.9.x, terraform plan does both: it refreshes state, then computes the plan against the refreshed state. The two steps are interleaved per resource — for each resource, refresh first, then plan.

For each resource in graph order:
    refresh  -> update internal state
    plan     -> compute diff for this resource

This is why a single terraform plan is enough to detect drift and propose the right corrective action.

-refresh-only: refresh without plan

terraform plan -refresh-only
terraform apply -refresh-only

refresh-only mode tells Terraform to perform only the refresh step. The output is a plan that proposes only state changes, no infrastructure changes:

aws_instance.web: Refreshing state... [id=i-0abc123]

Terraform will perform the following actions:

  # aws_instance.web will be updated in-place
  ~ resource "aws_instance" "web" {
        id           = "i-0abc123"
      ~ tags         = {
          + "Environment" = "production"
        }
        # (4 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.

Note the summary line: 0 to add, 0 to change, 0 to destroy. The plan output shows in-place updates to state, not to infrastructure. The apply, if approved, writes state but does not call any mutating provider RPC.

The production use case: an operator made an out-of-band change to a resource (a tag in the cloud console, a parameter on a database). Terraform’s state does not know about the change. The next plan would propose a change to undo the operator’s action, because Terraform sees the drift as a difference between configuration and state. terraform apply -refresh-only updates state to match reality, so the next plan shows no change.

-target interaction with refresh

terraform plan -target=aws_instance.web

-target filters which resources the plan considers. The filter also filters which resources are refreshed: only the targeted resources and their dependencies are refreshed; unrelated resources are not.

Two consequences:

  1. Drift in non-targeted resources is invisible. If a non-targeted resource has drifted in the real world, the plan does not see it (because it was not refreshed) and does not propose to update its state. The drift remains invisible.

  2. Targeting a downstream resource still refreshes its dependencies. Targeting aws_instance.web triggers refresh of aws_subnet.public because the instance references it.

A production rule: do not use -target to investigate drift. A drifted resource not covered by the target will be missed, and the operator will believe the drift does not exist.

-replace: forced destroy-create

terraform plan -replace=aws_instance.web

-replace forces Terraform to destroy the targeted resource and create a new one, regardless of whether the configuration has changed. The refresh still runs by default for the targeted resource.

The use cases:

  • A resource has accumulated state in the real world that cannot be updated in place (for example, an EC2 instance type that requires a stop).
  • A provider bug produced a state that cannot be reconciled with configuration by normal update operations.
  • An operator wants to verify a resource’s creation path by destroying and recreating it.

-replace does not bypass the plan. The plan still shows what will happen; the operator still approves.

When refresh hides real changes

Refresh has one production surprise: it can absorb out-of-band changes into state, making them invisible to subsequent plans.

Scenario:

  1. Configuration says tags = { Name = "web-01" }.
  2. State says tags = { Name = "web-01" } (from last refresh).
  3. An operator runs aws ec2 create-tags --resources i-0abc --tags Key=Environment,Value=production.
  4. A terraform plan runs. Refresh sees the new tag. State updates to tags = { Name = "web-01", Environment = "production" }.
  5. The plan computes the diff: configuration says Name = "web-01", state says Name = "web-01", Environment = "production". Terraform sees no change because the configuration’s Name matches.
  6. The next plan shows no change. The operator’s tag is now in state, invisible to the next person.

The hidden change is not gone — it is in state, and the next apply that touches the resource will preserve it. But the configuration does not mention it, and a terraform state show will display it. A new engineer reading the configuration has no way to know the resource has the Environment tag.

A production rule: refresh absorbs out-of-band changes into state. If the out-of-band change is intended, encode it in configuration. If it is unintended, use -target or -replace to address it.

Production failure modes

#Failure modeObservable symptomRecovery
1Refresh fails for one resource (provider timeout)Error: Failed to read resource state for that one resource; the rest of the plan continuesInvestigate the resource; re-run the plan; the failure is resource-specific
2-refresh=false skipped on a stale-state planPlan shows no changes but real world has driftedRe-plan without -refresh=false; investigate the actual state
3-refresh-only applied unintentionallyState updated but operator expected the apply to do nothingRestore state from backup if the change is not desired
4Provider’s read function does not return all attributesDrift in attributes not in the schema is invisibleCheck the provider’s source; some attributes are deliberately not surfaced
5Refresh takes very long (provider rate-limited, large state)terraform plan hangs on refreshUse -refresh=false for fast plan; run refresh separately; increase provider rate limits
6Drift in a resource not covered by -targetDrift invisible to the targeted planRun terraform plan without -target to refresh all resources; check terraform state show for the drifted resource

Security implications

  • Refresh reads from cloud APIs using the provider’s credentials. It uses the same credentials as apply, but it does not write anything. The credentials’ scope should still follow least privilege.
  • A refresh that runs unattended may expose drift to the wrong person. CI/CD plans with -target may refresh resources and write the updated state to the backend. The state file may now contain attributes the operator did not intend to reveal.
  • -refresh-only does not change the access pattern but does change the audit trail. The state file is rewritten, the serial is bumped, and the lineage may be preserved. The change is logged in the backend’s history.

Performance implications

  • Refresh is O(N) provider read calls. For a 5,000-resource state, refresh is 5,000 read API calls. With -parallelism=10, about 500 seconds of wall-clock time at 100 ms per call.
  • Refresh during plan and refresh during apply are not the same. Plan refresh runs against every resource. Apply refresh (1.3+) runs only against resources where state has changed since the plan. A plan from 09:00 saved and applied at 09:30 may trigger a refresh for the apply if any resource was modified externally.
  • -refresh=false makes plan much faster. For a large estate, disabling refresh cuts plan time by half or more. The trade-off is plan accuracy.

Production guidance

  1. Always enable refresh during plan. -refresh=false is a development convenience, not a production setting.
  2. Use -refresh-only after out-of-band changes. This is the intended use case.
  3. Do not use -target to investigate drift. A non-targeted resource’s drift will be missed.
  4. Be aware that refresh absorbs out-of-band changes. Encode intentional changes in configuration.
  5. Audit terraform plan output before applying. The plan shows refresh-driven state changes; do not let them pass silently.

Verification

  • What does refresh do, and what does it not do?
  • When does refresh run by default in Terraform 1.9.x, and when does it not?
  • What is the difference between -refresh-only and a normal plan?
  • How does -target interact with refresh, and what is the production risk?
  • How can refresh hide an out-of-band change from the next plan?

Knowledge check · 7 questions

  1. Q1. What does terraform refresh do?

  2. Q2. In Terraform 1.9.x, when does refresh run by default?

  3. Q3. terraform apply -refresh-only updates the state file without calling any mutating provider API.

  4. Q4. What is the production risk of using -target to investigate drift?

  5. Q5. Which of the following are part of the refresh-and-plan sequence? (Select all that apply.)

  6. Q6. How does refresh hide out-of-band changes?

  7. Q7. An operator manually adds a tag to an EC2 instance via the AWS console. A subsequent terraform plan shows no changes. What should the team do?

Passing score: 75%. Answers are checked in this browser.