hashicorp/terraform · error

failed to lock s3 state

Error message

failed to lock s3 state: %s

What it means

Thrown during Backend.StateMgr init in the S3 backend when a workspace does not yet exist and client.Lock(lockInfo) returns an error while taking the init lock. The lock guards the creation of the empty sentinel state object so Workspaces() lists it. %s wraps the underlying lock error (usually a DynamoDB-backed *statemgr.LockError).

Solutions

  1. Confirm `dynamodb_table` exists in the same region: `aws dynamodb describe-table --table-name <name>`.
  2. Inspect the wrapped error for 'workspace is already locked' - wait or `tofu force-unlock`.
  3. Grant the principal dynamodb:GetItem/PutItem/DeleteItem on the lock table ARN.
  4. Re-run `tofu init` after fixing config.
Defensive patterns

Strategy: validation

Validate before calling

// preflight: confirm the DynamoDB lock table exists and is writable
_, err := dbClient.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(lockTable)})
if err != nil { return fmt.Errorf("lock table %s missing or unreachable: %w", lockTable, err) }

Type guard

func isLockNotConfiguredErr(err error) bool {
    var le *statemgr.LockError
    return errors.As(err, &le) && strings.Contains(le.Err.Error(), "does not exist")
}

Try / catch

lockId, err := client.Lock(lockInfo)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) && le.Info != nil { return fmt.Errorf("state busy; run `tofu force-unlock %s`", le.Info.ID) }
    return err
}

Prevention

When it happens

Trigger: client.Lock(lockInfo) fails in the !exists branch. Causes: DynamoDB lock table missing or misconfigured, the state key is already locked by another run, IAM permissions lacking on the lock table, or DynamoDB throttling.

Common situations: Concurrent first-applies for a new workspace; `dynamodb_table` pointing at a non-existent table; principal lacks dynamodb:GetItem/PutItem/DeleteItem; region mismatch between S3 and DynamoDB.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/s3/backend_state.go:217

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