hashicorp/terraform · error · statemgr.LockError

Already locked for workspace creation: %s

Error message

Already locked for workspace creation: %s

What it means

Returned by RemoteClient.Lock (pg/client.go:112) in the workspace-creation branch. When the SELECT finds no row (sql.ErrNoRows) Terraform tries to take the global creation advisory lock pg_try_advisory_lock(-1); if it returns false, another session is already creating a workspace, so a typed *statemgr.LockError with this message is returned.

Source

Thrown at internal/backend/remote-state/pg/client.go:112

		return nil
	}

	// Try to acquire locks for the existing row `id` and the creation lock `-1`.
	query := `SELECT %s.id, pg_try_advisory_lock(%s.id), pg_try_advisory_lock(-1) FROM %s.%s WHERE %s.name = $1`
	row := c.Client.QueryRow(fmt.Sprintf(query, statesTableName, statesTableName, c.SchemaName, statesTableName, statesTableName), c.Name)
	var pgLockId, didLock, didLockForCreate []byte
	err = row.Scan(&pgLockId, &didLock, &didLockForCreate)
	switch {
	case err == sql.ErrNoRows:
		// No rows means we're creating the workspace. Take the creation lock.
		innerRow := c.Client.QueryRow(`SELECT pg_try_advisory_lock(-1)`)
		var innerDidLock []byte
		err := innerRow.Scan(&innerDidLock)
		if err != nil {
			return "", &statemgr.LockError{Info: info, Err: err}
		}
		if string(innerDidLock) == "false" {
			return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Already locked for workspace creation: %s", c.Name)}
		}
		info.Path = "-1"
	case err != nil:
		return "", &statemgr.LockError{Info: info, Err: err}
	case string(didLock) == "false":
		// Existing workspace is already locked. Release the attempted creation lock.
		lockUnlock("-1")
		return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Workspace is already locked: %s", c.Name)}
	case string(didLockForCreate) == "false":
		// Someone has the creation lock already. Release the existing workspace because it might not be safe to touch.
		lockUnlock(string(pgLockId))
		return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Cannot lock workspace; already locked for workspace creation: %s", c.Name)}
	default:
		// Existing workspace is now locked. Release the attempted creation lock.
		lockUnlock("-1")
		info.Path = string(pgLockId)
	}
	c.info = info

View on GitHub (pinned to c9def3e214)

Solutions

  1. Retry after the other creation finishes — the -1 lock is short-lived.
  2. Find the holder: SELECT * FROM pg_locks WHERE locktype='advisory' AND objid=-1; join pg_stat_activity and pg_terminate_backend() if stale.
  3. Serialize workspace creation in your automation.
Defensive patterns

Strategy: try-catch

Validate before calling

// Before creating a workspace, check no one holds the creation lock
// SELECT 1 FROM pg_locks WHERE locktype='advisory' AND classid=0 AND objid=-1;

Type guard

// RemoteClient.Lock returns a typed lock error you can narrow on
// var lockErr *statemgr.LockError
// if errors.As(err, &lockErr) {
//   // lockErr.Info holds who currently holds the creation lock
// }

Try / catch

// Creation-lock races are safe to retry after backoff
// for i := 0; i < 5; i++ {
//   _, err := client.Lock(info)
//   var le *statemgr.LockError
//   if !errors.As(err, &le) || !strings.Contains(le.Error(), "workspace creation") {
//     return err
//   }
//   time.Sleep(time.Duration(1<<i) * time.Second)
// }

Prevention

When it happens

Trigger: Two clients concurrently creating the same (or any) new workspace — only one can hold the -1 creation lock at a time. Also fires if a previous create crashed but its DB connection still holds the -1 advisory lock.

Common situations: Parallel CI pipelines each bootstrapping a workspace; a team automation race; a prior Terraform run was OOM-killed leaving a live Postgres session holding -1.

Related errors


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