hashicorp/terraform · error

failed to lock OSS state: %s

Error message

failed to lock OSS state: %s

What it means

In StateMgr() (backend_state.go:149-151), when initializing a brand-new workspace state, client.Lock(lockInfo) failed. Lock() (client.go:153) attempts a conditional PutRow into TableStore; on failure it returns a statemgr.LockError whose message is wrapped here. This means another process holds the lock or the OTS write was rejected.

Source

Thrown at internal/backend/remote-state/oss/backend_state.go:151

	}

	log.Printf("[DEBUG] Current workspace name: %s. All workspaces:%#v", name, existing)

	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 := client.Lock(lockInfo)
		if err != nil {
			return nil, diags.Append(fmt.Errorf("failed to lock OSS state: %s", err))
		}

		// Local helper function so we can call it multiple places
		lockUnlock := func(e error) error {
			if err := stateMgr.Unlock(lockId); err != nil {
				return fmt.Errorf(strings.TrimSpace(stateUnlockError), lockId, err)
			}
			return e
		}

		// Grab the value
		if err := stateMgr.RefreshState(); err != nil {
			err = lockUnlock(err)
			return nil, diags.Append(err)
		}

		// If we have no state, we have to create an empty state
		if v := stateMgr.State(); v == nil {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Run terraform force-unlock <LOCK_ID> using the lock ID from the error to clear the stale row.
  2. Serialize workspace initialization (one runner/branch at a time) to avoid concurrent init races.
  3. Confirm the credentials have tablestore:PutRow and tablestore:GetRow on the OTS table.
  4. If OTS is throttling, retry after a brief backoff or raise the instance throughput quota.

Example fix

# before: stale lock blocks init
terraform init

# after
terraform force-unlock a1b2c3d4-...
terraform init
Defensive patterns

Strategy: try-catch

Validate before calling

// Before init, check whether a lock row already exists for this state path.
func lockHeld(c *tablestore.TableStoreClient, table, lockPath string) bool {
    _, err := c.GetRow(&tablestore.GetRowRequest{SingleRowQueryCriteria: &tablestore.SingleRowQueryCriteria{
        TableName: table,
        PrimaryKey: &tablestore.PrimaryKey{PrimaryKeys: []*tablestore.PrimaryKeyColumn{
            {ColumnName: "LockID", Value: lockPath},
        }},
        MaxVersion: 1,
    }})
    return err == nil
}

Try / catch

// On init, if the error indicates a held lock, prompt for force-unlock.
if err != nil && strings.Contains(err.Error(), "failed to lock OSS state") {
    fmt.Println("state is locked; run: terraform force-unlock <ID>")
    return err
}

Prevention

When it happens

Trigger: StateMgr creating a not-yet-existing workspace takes an init lock; the OTS PutRow with RowExistenceExpectation_EXPECT_NOT_EXIST fails because a lock row already exists (concurrent init, stale lock from a crashed run) or because of an OTS service/permission error.

Common situations: Two CI runners initializing the same workspace simultaneously; a previous terraform process was killed leaving a stale lock row; OTS throttling; RAM policy lacks tablestore:PutRow.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/8bb340a8851a9f33. Report an issue: GitHub.