hashicorp/terraform · critical
error uploading state
Error message
error uploading state: %w
What it means
Thrown during PersistState when the uploadState method fails to push the serialized state (raw, JSON, and JSON outputs) to HCP Terraform / TFE via the StateVersions.Upload API. On failure, s.stateUploadErr is set to true, which intentionally prevents the workspace from being unlocked so that a subsequent apply cannot run against stale state. This is the primary network/authorization error for remote state persistence.
Solutions
- Check network connectivity and DNS resolution to your HCP Terraform / TFE endpoint
- Verify the API token is valid and not expired (terraform login or TF_TOKEN / credentials)
- Ensure no other process is applying to the same workspace concurrently (check the run queue and workspace lock)
- Confirm the authenticated user/team has write permissions on the workspace
- Retry the operation after a brief wait if the wrapped error indicates a transient 5xx
- If on self-hosted TFE, check the TFE instance health and that the state version upload API is supported
Defensive patterns
Strategy: retry
Validate before calling
// Before persisting, verify the TFE client can reach the workspace:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if _, err := tfeClient.Workspaces.Read(ctx, organization, workspaceName); err != nil {
return fmt.Errorf("pre-persist workspace check failed, upload would likely fail: %w", err)
} Try / catch
// Retry PersistState for transient upload errors, but surface conflicts:
backoff := time.Second
for attempt := 0; attempt < 3; attempt++ {
err := stateMgr.PersistState(schemas)
if err == nil {
break
}
msg := err.Error()
if strings.Contains(msg, "409") || strings.Contains(msg, "conflict") {
return err // lineage/serial conflict — do NOT retry blindly
}
if isTransientError(msg) {
time.Sleep(backoff)
backoff *= 2
continue
}
return err
} Prevention
- Ensure exactly one process applies to each workspace at a time (use CI workspace queueing or external locking)
- Keep API tokens fresh and rotate them with overlap, not cut-over
- Monitor HCP Terraform status page before large-scale apply operations
- Verify workspace permissions include write access for the CI service account
When it happens
Trigger: Network failure or timeout during the state version upload HTTP request; invalid or expired API token; workspace locked by another run causing a 409 conflict; lineage/serial mismatch (concurrent modification); TFE server 5xx error; workspace permissions insufficient for the authenticated user; old TFE version lacking the Upload endpoint triggering the fallback path which also fails.
Common situations: Intermittent network connectivity to app.terraform.io or a self-hosted TFE instance; expired/rotated API tokens after credential management changes; two CI pipelines running apply concurrently against the same workspace; TFE instance under load returning 502/503; user with read-only workspace membership attempting apply.
Related errors
- error retrieving state
- could not read state version output
- could not read state version outputs
- Error downloading state
- error downloading state
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/dd062ccdf7b67ac7.
Report an issue: GitHub.
Appendix: source
Thrown at internal/cloud/state.go:234
stateFile, err := statefile.Read(bytes.NewReader(buf.Bytes()))
if err != nil {
return fmt.Errorf("failed to read state: %w", err)
}
ov, err := jsonstate.MarshalOutputs(stateFile.State.RootOutputValues)
if err != nil {
return fmt.Errorf("failed to translate outputs: %w", err)
}
jsonStateOutputs, err := json.Marshal(ov)
if err != nil {
return fmt.Errorf("failed to marshal outputs to json: %w", err)
}
err = s.uploadState(s.lineage, s.serial, s.forcePush, buf.Bytes(), jsonState, jsonStateOutputs)
if err != nil {
s.stateUploadErr = true
return fmt.Errorf("error uploading state: %w", err)
}
// After we've successfully persisted, what we just wrote is our new
// reference state until someone calls RefreshState again.
// We've potentially overwritten (via force) the state, lineage
// and / or serial (and serial was incremented) so we copy over all
// three fields so everything matches the new state and a subsequent
// operation would correctly detect no changes to the lineage, serial or state.
s.readState = s.state.DeepCopy()
s.readLineage = s.lineage
s.readSerial = s.serial
return nil
}
// ShouldPersistIntermediateState implements statemgr.IntermediateStateConditionalPersister
func (s *State) ShouldPersistIntermediateState(info *statemgr.IntermediateStatePersistInfo) bool {
if info.ForcePersist {
return trueView on GitHub (pinned to d32a084675)