hashicorp/terraform · error · statemgr.LockError
writing %q failed: %v
Error message
writing %q failed: %v
What it means
remoteClient.Lock() writes the .tflock object with a DoesNotExist precondition (so only one writer can win). If Write or Close on that conditional writer fails, the error is wrapped through lockError into a *statemgr.LockError. Most commonly this is the precondition failure meaning someone else already holds the lock; it also fires on permission/quota/network errors.
Source
Thrown at internal/backend/remote-state/gcs/client.go:118
// we can't set the ID until the info is written
info.Path = c.lockFileURL()
infoJson, err := json.Marshal(info)
if err != nil {
return "", err
}
lockFile := c.lockFile()
w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)
err = func() error {
if _, err := w.Write(infoJson); err != nil {
return err
}
return w.Close()
}()
if err != nil {
return "", c.lockError(fmt.Errorf("writing %q failed: %v", c.lockFileURL(), err))
}
info.ID = strconv.FormatInt(w.Attrs().Generation, 10)
return info.ID, nil
}
func (c *remoteClient) Unlock(id string) error {
ctx := context.TODO()
gen, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return fmt.Errorf("Lock ID should be numerical value, got '%s'", id)
}
if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(ctx); err != nil {
return c.lockError(err)
}View on GitHub (pinned to c9def3e214)
Solutions
- If the wrapped error indicates precondition/412, another process holds the lock: wait for it to finish or 'terraform force-unlock <ID>' only if it's truly stale.
- Confirm SA has storage.objects.create on the bucket (needed to create .tflock).
- Check the existing lock: 'gsutil cat gs://<bucket>/<prefix>/<ws>.tflock' to see who holds it (LockInfo with Operation, Who, Created).
- Prevent concurrency: serialize CI for a given workspace; use distinct workspaces for parallel pipelines.
Example fix
# recover from stale lock gsutil cat gs://bucket/prefix/default.tflock # inspect holder terraform force-unlock 1681234567890 # generation-based ID terraform apply
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check whether a lock exists
ctx := context.Background()
_, err := client.Bucket(bucket).Object(lockPath).Attrs(ctx)
if err == nil { return fmt.Errorf("workspace already locked; check .tflock") } Try / catch
_, err := client.Lock(info)
if le, ok := err.(*statemgr.LockError); ok {
// inspect le.Info to surface holder; offer force-unlock with the generation
} Prevention
- Serialize CI per workspace to avoid lock contention.
- Always surface lock holder info so operators can decide wait vs force-unlock.
- Confirm SA has storage.objects.create for the lock path.
When it happens
Trigger: 'terraform apply' / init on a fresh workspace where the .tflock already exists (another apply/plan/init is running) → precondition 412; or SA lacks storage.objects.create on the prefix; or transient GCS error during the lock write.
Common situations: Concurrent CI jobs on the same workspace; a crashed prior run left the .tflock behind; SA missing create permission on the lock path; someone is running a long apply.
Related errors
- %v Additionally, unlocking the state file on Google Clou
- Lock ID should be numerical value, got '%s'
- querying Cloud Storage failed: %v
- Failed to open state file at %v: %v
- Failed to delete state file %v: %v
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/95fde91f955a379b.
Report an issue: GitHub.