
Updating state in a remote backend comes down to two questions: is your backend actually remote, and which of the several ways to update state fits what you're trying to fix? First confirm the backend, then pick the method.
None of this works against the default local backend, there's no separate remote copy to pull, push, or conflict with, so confirm the backend first. "Remote" and "cloud" here mean two specific Terraform config blocks, not a general description of any non-local backend.
remote backend type (Scalr)Scalr's standard setup, and older HCP Terraform configurations, use backend "remote":
terraform {
backend "remote" {
hostname = "<my-account>.scalr.io"
organization = "<ID of environment>"
workspaces {
name = "<workspace-name>"
}
}
}cloud block (HCP Terraform, and Scalr for tag-based workspaces)Current HCP Terraform configurations use a dedicated cloud block instead of backend "remote". Scalr accepts it too, specifically when you want to select a workspace by tags rather than by name:
terraform {
cloud {
hostname = "<my-account>.scalr.io"
organization = "<ID of environment>"
workspaces {
tags = ["<your-tags>"]
}
}
}After terraform init, Scalr looks for workspaces in that environment matching the tags: one match auto-selects it, several prompt you to choose, none prompts you to create a new one. This is the one place Scalr's config diverges from plain backend "remote", which only supports selecting a workspace by name, not by tags.
S3, GCS, and azurerm are also remote in the everyday sense of the word, your state isn't on your laptop, but they're their own backend types (backend "s3", backend "gcs", backend "azurerm"), not the remote or cloud block. They also work differently once you get to Step 2: they treat the state file as a single object in a bucket, with no workspace concept or run history layered on top, and locking is bolted on separately (a DynamoDB table for S3, native locking for GCS and azurerm) rather than being part of the same system that tracks versions. The remote and cloud blocks add a workspace-level API on top of that same interface, with retrievable version history and, on Scalr, permission checks (state-versions:read to view, separate access to write) before anything lands.
Run terraform init after adding a backend or cloud block. Terraform reports what it connected to, and if you had local state already, it offers to copy it up. Confirm the connection actually landed with:
terraform state pull
Against a remote or cloud backend this downloads the real state from Scalr, HCP Terraform, S3, or wherever it lives. Against the local backend (no block, or a backend "local" block) it just reads whatever terraform.tfstate file happens to be sitting next to your code, which is the tell that you're not actually remote yet.
Seven ways, roughly in order of how often you'll reach for each.
terraform state mvUse this when a resource's address changed, moving into a module, a for_each, or a rename, but the underlying infrastructure didn't:
terraform state mv aws_instance.web module.compute.aws_instance.web
Runs directly against the remote backend. No pull/push needed.
terraform state rmUse this when infrastructure was deleted outside Terraform (someone removed it in the console) or you're deliberately handing a resource off to a different state file:
terraform state rm aws_instance.old_bastion
This only edits state. If the resource still exists and is still in your code, the next apply will try to recreate it.
terraform importUse this when a resource exists in the cloud and in your code, but Terraform has never tracked it:
terraform import aws_instance.web i-0d13d5591a41e18ef
Works the same against a remote backend as a local one; no special steps. The Terraform import guide covers import {} blocks and bulk imports in more depth.
moved blockUse this for the same rename/relocate case as state mv, but as code that ships through a normal pull request instead of a one-off command someone has to remember to run:
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}Apply it and Terraform updates state to match. Nothing to push manually.
terraform apply -refresh-onlyUse this when you suspect drift, someone changed a tag or a setting directly in the cloud console, and want state to reflect reality before you decide whether to fix it in code or accept it:
terraform apply -refresh-only
Shows you what changed and asks for confirmation before writing anything.
Use this only when nothing above covers the case, a corrupted attribute, merging state from two sources by hand:
terraform state pull > terraform.tfstate
# edit terraform.tfstate
terraform state push terraform.tfstate
This is the one that fails the first time almost everyone tries it. Every state file carries a serial number that increments on every write and a lineage UUID identifying its history; Terraform refuses the push if your file's serial isn't higher than what's already stored remotely, or if the lineage doesn't match. The error looks like this:
Failed to write state: cannot overwrite existing state with serial 1 with a different state that has the same serial.
Fix it by bumping serial in your local copy to at least one higher than the remote value before pushing again. If the remote shows "serial": 3, set yours to 4:
{
"version": 4,
"terraform_version": "1.5.7",
"serial": 4,
...
}Both checks can be bypassed with terraform state push -force, which skips the lineage and serial comparisons entirely. Don't, unless you're certain the state you're pushing should win; it silently overwrites whatever's remote, including changes you didn't know about.
Before you push a manual edit: back it up first (terraform state pull > backup.tfstate, redundant on Scalr since every push already versions, but the only safety net at all on an object-storage backend without bucket versioning turned on); pause anything else that could write to the same state, a scheduled run, a teammate's apply, a CI pipeline; and run terraform plan right after the push lands, a clean plan means your edit and reality still agree, a plan that wants to create or destroy things you didn't touch means it doesn't.
On a remote-operations backend you're not limited to the CLI. Scalr's workspace State tab has an "Upload state file" action, and every past version has a one-click rollback: pick an older version, roll back, and nothing about your actual infrastructure changes until the next run detects a difference. Object-storage backends have no equivalent; recovering a bad push to an S3 bucket means whatever you set up ahead of time, bucket versioning, most commonly, restored through the AWS console or CLI rather than a purpose-built UI.
A stuck lock isn't an update, but it'll block one. Clear it with:
terraform force-unlock LOCK_ID
using the lock ID from the error message. It only clears the lock and never touches infrastructure, but confirm nothing is actually running first, force-unlocking a genuinely active plan or apply can let two writes race. What the lock physically is depends on the backend: a DynamoDB row for S3, a native lease for GCS and azurerm, a workspace-level flag for Scalr and HCP Terraform. If lock errors are a recurring problem rather than a one-off, the state lock errors guide covers root causes and prevention in more depth than fits here.
For the broader command reference beyond what's above, taint/untaint included, see the guide to updating Terraform state. If what actually broke is an empty or corrupted state file rather than a normal edit, that's a different recovery path, covered in the empty state file recovery guide.
