hashicorp/terraform · error
failed to lock oci state: %s
Error message
failed to lock oci state: %s
What it means
Thrown by StateMgr when b.client.Lock fails during initial state creation for a workspace that does not yet exist. The OCI lock is implemented as a PutObject on the lock file with IfNoneMatch:"*" (client.go:275), so this fails whenever a lock-file object already exists at the computed lockFilePath. The error wraps the underlying lock failure, which is usually a statemgr.LockError carrying the Info of the current holder.
Source
Thrown at internal/backend/remote-state/oci/backend_state.go:74
return nil, diags
}
exists := false
for _, s := range existing {
if s == name {
exists = true
break
}
}
// We need to create the object so it's listed by States.
if !exists {
// take a lock on this state while we write it
lockInfo := statemgr.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := b.client.Lock(lockInfo)
if err != nil {
return nil, diags.Append(fmt.Errorf("failed to lock oci state: %s", err))
}
// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}
return parent
}
// Grab the value
// This is to ensure that no one beat us to writing a state between
// the `exists` check and taking the lock.
if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, diags.Append(err)
}
View on GitHub (pinned to d32a084675)
Solutions
- Wait for the in-progress operation to finish, then retry init — the lock will be released normally.
- Confirm no operation is actually running, then run `terraform force-unlock <LOCK_ID>` using the ID from the wrapped LockError.Info.ID.
- If force-unlock cannot fetch the lock (see errors 310-313), delete the lock object directly in the OCI bucket at path <workspaceKeyPrefix>/<name>.tflock.
- Serialize init/apply on shared workspaces with an external mutex (CI lock, OCI lock via a separate object).
Example fix
// before: two CI jobs run `terraform init` concurrently on the same workspace // after: gate the init with an external lock so only one job initializes at a time // - lock: "tf-init-$WORKSPACE" // run: terraform init && terraform apply -auto-approve // recovery for an orphaned lock: // terraform force-unlock <LOCK_ID_FROM_ERROR>
Defensive patterns
Strategy: retry
Validate before calling
// Before acquiring the lock, check whether a lock object already exists.
func lockExists(c *RemoteClient, ctx context.Context) (bool, error) {
_, err := c.objectStorageClient.HeadObject(ctx, objectstorage.HeadObjectRequest{
NamespaceName: common.String(c.namespace),
BucketName: common.String(c.bucketName),
ObjectName: common.String(c.lockFilePath),
})
if err == nil { return true, nil }
var se common.ServiceError
if errors.As(err, &se) && se.GetHTTPStatusCode() == 404 { return false, nil }
return false, err
} Type guard
// Distinguish a lock contention from other failures
func isLockBusy(err error) bool {
var le *statemgr.LockError
return errors.As(err, &le)
} Try / catch
lockId, err := b.client.Lock(lockInfo)
if err != nil {
var le *statemgr.LockError
if errors.As(err, &le) && le.Info != nil {
// surface the holder's ID so the user can decide/force-unlock
return fmt.Errorf("locked by %s (op=%s) since %s; force-unlock %s",
le.Info.ID, le.Info.Operation, le.Info.Created, le.Info.ID)
}
return err
} Prevention
- Serialize init/apply on shared workspaces with an external mutex (CI lock, separate OCI object).
- Educate operators to use `terraform force-unlock <ID>` rather than deleting resources manually.
- Monitor for orphaned .tflock objects and alert on lock files older than N hours.
- Confirm no other run is active before force-unlocking.
When it happens
Trigger: Two concurrent `terraform init` runs against the same not-yet-created workspace; a previous process crashed holding the lock; a stale .tflock object remains in the bucket; running init while another user runs apply on the same workspace.
Common situations: CI pipelines racing on a shared workspace; a killed/OOM-killed run leaving the lock object behind; manual bucket inspection showing an orphaned `<workspace>.tflock` file; switching a workspace from another backend leaving stale locks.
Related errors
- Error unlocking oci state. Lock ID: %s Error: %s You may h
- failed to get existing lock file: %w
- lock ID '%s' does not match the existing lock ID '%s'
- errRunApproved
- errRunDiscarded
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/8201714c27600b3f.
Report an issue: GitHub.