hashicorp/terraform · error · statemgr.LockError

state blob is already locked

Error message

state blob is already locked

What it means

Raised in RemoteClient.Lock (client.go:194-197). Before acquiring a lease, the backend checks the blob's LeaseStatus; if it is already 'Locked', another Terraform process (or a manual lease) holds it, so Lock refuses and returns a LockError carrying the existing lock info. This is the primary concurrency-protection mechanism for Azure remote state.

Source

Thrown at internal/backend/remote-state/azure/client.go:196

		if !response.WasNotFound(properties.HttpResponse) {
			return "", getLockInfoErr(err)
		}
		// if we don't find the blob, we need to build it

		contentType := "application/json"
		putGOptions := blobs.PutBlockBlobInput{
			ContentType: &contentType,
		}

		_, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.containerName, c.keyName, putGOptions)
		if err != nil {
			return "", getLockInfoErr(err)
		}
	}

	// if the blob is already locked then error
	if properties.LeaseStatus == blobs.Locked {
		return "", getLockInfoErr(fmt.Errorf("state blob is already locked"))
	}

	leaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.containerName, c.keyName, leaseOptions)
	if err != nil {
		return "", getLockInfoErr(err)
	}

	info.ID = leaseID.LeaseID
	c.leaseID = leaseID.LeaseID

	if err := c.writeLockInfo(info); err != nil {
		return "", err
	}

	return info.ID, nil
}

func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Wait for the current Terraform process to finish, then retry
  2. If the lock is stale, run 'terraform force-unlock <lock-id>' (the error includes the existing lock info)
  3. Break the lease directly: az storage blob lease break --account-name <account> -c <container> -b <key>
  4. Add CI queueing/serialization so only one run targets a workspace at a time

Example fix

# the LockError prints who holds the lock; force-unlock if stale
terraform force-unlock <lock-id>

# or break the Azure lease
az storage blob lease break \
  --account-name mystage --auth-mode login \
  -c tfstate -b "prod.terraform.tfstate"
Defensive patterns

Strategy: retry

Validate before calling

# Pre-check lease status before applying
STATUS=$(az storage blob show --account-name "$ARM_STORAGE_ACCOUNT_NAME" -c "$ARM_CONTAINER_NAME" -n "$ARM_KEY" --query 'properties.lease.status' -o tsv 2>/dev/null)
[ "$STATUS" = "locked" ] && echo "WARN: already locked -> error 154; coordinate with holder or force-unlock" || echo "OK: lockable"

Try / catch

# Wait for a lock to clear, then optionally force-unlock
await_lock() {
  for attempt in $(seq 1 6); do
    terraform apply -auto-approve && return 0
    grep -q "state blob is already locked" || return 1
    sleep 30
  done
  LID=$(grep -oE '[0-9a-f-]{36}' err.log | head -1)
  echo "Still locked after retries; review then: terraform force-unlock $LID"
}

Prevention

When it happens

Trigger: Produced at client.go:195-196 when properties.LeaseStatus == blobs.Locked at lock time, e.g. two concurrent 'terraform apply' on the same workspace, or a previous crashed run left the lease.

Common situations: Concurrent CI jobs on the same state file; a long-running apply blocking another; a previous Terraform crash/OOM-kill leaving the lease; a manual developer lock.

Related errors


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