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

  1. Wait for the in-progress operation to finish, then retry init — the lock will be released normally.
  2. Confirm no operation is actually running, then run `terraform force-unlock <LOCK_ID>` using the ID from the wrapped LockError.Info.ID.
  3. 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.
  4. 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

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


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/8201714c27600b3f. Report an issue: GitHub.