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
- Wait for the current Terraform process to finish, then retry
- If the lock is stale, run 'terraform force-unlock <lock-id>' (the error includes the existing lock info)
- Break the lease directly: az storage blob lease break --account-name <account> -c <container> -b <key>
- 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
- Queue CI jobs per workspace so concurrent applies don't contend on the lease
- After a crashed run, force-unlock before the next attempt
- Use 'az storage blob lease status' monitoring for shared-state pipelines
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
- failed to lock azure state: %s
- Error unlocking Azure state. Lock ID: %s Error: %s You may
- blob metadata %q was empty
- failed to retrieve lock info: %s
- lock id %q does not match existing lock
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/3c0372796b66718c.
Report an issue: GitHub.