hashicorp/terraform · error

state blob is already locked

Error message

state blob is already locked

What it means

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.

Solutions

  1. Run `terraform force-unlock <id>` using the ID in the LockError info.
  2. Confirm no legitimate run is in progress before force-unlocking.
  3. If the lock info cannot be read (see error 158), break the lease via `az storage blob lease break`.
  4. Coordinate team access to the shared state to prevent recurrence.
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: check lease status before Lock.
func blobLeaseStatus(ctx context.Context, acct, container, blob string) (string, error) {
    cmd := exec.CommandContext(ctx, "az", "storage", "blob", "show",
        "--account-name", acct, "-c", container, "-n", blob, "--query", "properties.lease.status", "-o", "tsv")
    out, err := cmd.Output()
    return strings.TrimSpace(string(out)), err
}

Try / catch

// Catch LockError, print the holder's lock info, and offer force-unlock.
lockId, err := client.Lock(info)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) {
        if le.Info != nil {
            log.Printf("state already locked by %s (%s); run: terraform force-unlock %s",
                le.Info.Who, le.Info.Operation, le.Info.ID)
        }
        os.Exit(1)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: Concurrent runs in different terminals / CI pipelines; crashed run left a lease; a teammate's interrupted apply.

Related errors


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

Appendix: 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 d32a084675)