{"record":{"id":"3c0372796b66718c","repo":"hashicorp/terraform","slug":"state-blob-is-already-locked","errorCode":null,"errorMessage":"state blob is already locked","messagePattern":"state blob is already locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/azure/client.go","lineNumber":196,"sourceCode":"\t\tif !response.WasNotFound(properties.HttpResponse) {\n\t\t\treturn \"\", getLockInfoErr(err)\n\t\t}\n\t\t// if we don't find the blob, we need to build it\n\n\t\tcontentType := \"application/json\"\n\t\tputGOptions := blobs.PutBlockBlobInput{\n\t\t\tContentType: &contentType,\n\t\t}\n\n\t\t_, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.containerName, c.keyName, putGOptions)\n\t\tif err != nil {\n\t\t\treturn \"\", getLockInfoErr(err)\n\t\t}\n\t}\n\n\t// if the blob is already locked then error\n\tif properties.LeaseStatus == blobs.Locked {\n\t\treturn \"\", getLockInfoErr(fmt.Errorf(\"state blob is already locked\"))\n\t}\n\n\tleaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.containerName, c.keyName, leaseOptions)\n\tif err != nil {\n\t\treturn \"\", getLockInfoErr(err)\n\t}\n\n\tinfo.ID = leaseID.LeaseID\n\tc.leaseID = leaseID.LeaseID\n\n\tif err := c.writeLockInfo(info); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn info.ID, nil\n}\n\nfunc (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/azure/client.go#L178-L214","documentation":"Returned by RemoteClient.Lock when the blob's GetProperties response has LeaseStatus == blobs.Locked. The backend short-circuits with this message wrapped into a LockError that includes the existing lock info (retrieved via getLockInfo). This is the canonical 'another run holds the state' signal.","triggerScenarios":"Another terraform process is mid-apply on the same state blob; a previous run crashed leaving a stale lease; the blob was leased out-of-band.","commonSituations":"Concurrent runs in different terminals / CI pipelines; crashed run left a lease; a teammate's interrupted apply.","solutions":["Run `terraform force-unlock <id>` using the ID in the LockError info.","Confirm no legitimate run is in progress before force-unlocking.","If the lock info cannot be read (see error 158), break the lease via `az storage blob lease break`.","Coordinate team access to the shared state to prevent recurrence."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-flight: check lease status before Lock.\nfunc blobLeaseStatus(ctx context.Context, acct, container, blob string) (string, error) {\n    cmd := exec.CommandContext(ctx, \"az\", \"storage\", \"blob\", \"show\",\n        \"--account-name\", acct, \"-c\", container, \"-n\", blob, \"--query\", \"properties.lease.status\", \"-o\", \"tsv\")\n    out, err := cmd.Output()\n    return strings.TrimSpace(string(out)), err\n}","typeGuard":null,"tryCatchPattern":"// Catch LockError, print the holder's lock info, and offer force-unlock.\nlockId, err := client.Lock(info)\nif err != nil {\n    var le *statemgr.LockError\n    if errors.As(err, &le) {\n        if le.Info != nil {\n            log.Printf(\"state already locked by %s (%s); run: terraform force-unlock %s\",\n                le.Info.Who, le.Info.Operation, le.Info.ID)\n        }\n        os.Exit(1)\n    }\n    return err\n}","preventionTips":["Run a single terraform process per workspace at a time.","Wire CI to acquire a workspace lock outside terraform if multiple pipelines share state.","Document `terraform force-unlock` as a runbook step."],"tags":["azure","state-locking","lease","concurrency","force-unlock"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}