
## Release a stuck lock

`DELETE /v1/infra/servers/:id/lock`

Forcibly releases a stuck operation lock on the server. Use it when [`/exec`](./exec.md) or [`/deploy`](./deploy.md) was aborted from the client side (lost connection, local timeout), but the lock is still active on the server and subsequent calls return `EXEC_BUSY`. This is the LAST recovery step, not the first: the lock is released UNCONDITIONALLY, so it comes after retries at the `Retry-After` interval and the guarded [`/unstick`](/docs/infra/servers/unstick), and only once the server is independently confirmed idle — by its state and its logs. The endpoint always returns 200: if a lock was held on this replica — it is released, the response carries `released: true` and `localLock: true`; if not — `released: false` (also a success).

The recovery sequence below is performed by the holder of the key that manages the server. Development-team keys can call lock release but cannot read the operation list/status or call `/unstick`; ask the managing-key holder to perform those steps. Team callers must also obtain independent proof that the server is idle before releasing a lock. A route permission alone never proves idleness.

The call has two different scopes, and they must not be conflated. ON THIS replica the release is unconditional — which is why the independent idleness check belongs precisely here. The broadcast to **all** replicas (`broadcast: true`) is narrower, and deliberately so: a peer releases the lock only if it holds it itself and only if it is an `exec` one; a live foreign `deploy` is left alone. So the endpoint does clear a stuck `exec` lock held on another replica — which is exactly why `released` can be `false` while the lock really was held and released on the holder replica. `released`/`localLock` describe only the current replica and are **not proof of a fleet-wide release** — do not poll the endpoint in a loop until `released: true`. Instead, read the previous operation outcome before deciding whether to retry (`/exec`/`/deploy`). An `EXEC_BUSY` that survives this call does NOT by itself prove the agent is holding it: the broadcast deliberately leaves a foreign live `deploy` lock alone, so it may equally be a deploy on another replica. `error.hint` tells them apart: with `autoExpiresInSeconds` it is the platform lock and the thing to wait out; without it, a synchronous command or, on `STANDALONE`, a durable job may occupy the agent channel. First read the [server operations](./operations.md) with `kind=exec` and their [outcomes](./operation-status.md). The absence of that field does not prove a leaked mutex. The guarded [`/unstick`](/docs/infra/servers/unstick) is appropriate only for a confirmed leaked synchronous mutex on an independently confirmed idle dedicated virtual machine (`kind: "STANDALONE"`). On a galaxy host and a Galaxy application the call returns `409 GALAXY_UNSTICK_UNSUPPORTED`, because the exec channel is shared; there the remaining options are to retry at the `Retry-After` interval and, if the refusal persists, to contact support. The broadcast is best-effort (a no-op when only one replica is active, or if the bus is down); a guaranteed release of a stuck `exec` lock comes from the server-side TTL auto-sweep (below).

> **Use with care.** Releasing the lock of an active operation can lead to an unpredictable server state — `/exec` and `/deploy` running at the same time may compete for files and systemd units.

On `STANDALONE`, a running durable job keeps the execution channel busy after `202` releases the platform lock. Poll its operation; an unknown outcome needs support and independently confirmed full termination. Missing `autoExpiresInSeconds` does not prove a leaked mutex. Recovering the tunnel or releasing the lock does not remove the restriction on starting another durable job. Galaxy commands run synchronously with a 600-second limit and share the host channel across all applications and deploys; your own operation list may not show another application’s job, so an empty list does not prove the host is idle.

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | Server ID. List: [`GET /v1/infra/servers`](/docs/infra/servers/list). The ID of a deleted server managed by this key appears in the list with `includeDeleted=true` |

The request body is empty.

## Examples

### curl — personal key

```bash
curl -X DELETE -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/lock
```

### curl — OAuth application

```bash
curl -X DELETE -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/lock
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/lock`,
  {
    method: 'DELETE',
    headers: { 'X-Api-Key': 'YOUR_API_KEY' },
  }
)
const { data } = await res.json()

if (data.released) {
  console.log(`Released lock ${data.operation}, held for ${data.ageMs}ms, ${data.expiresInMs}ms left before expiry`)
} else {
  // No local lock, but the release was broadcast to other replicas (data.broadcast === true).
  console.log(data.message)
}
```

### JavaScript — OAuth application

```javascript
await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/lock`,
  {
    method: 'DELETE',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` when called on an existing server |
| `data.released` | boolean | `true` — a lock was held on this replica and it was released. `false` — no local lock. **Describes only the current replica**, not the whole fleet |
| `data.localLock` | boolean | Whether the lock was held on this very replica (`true` when `released: true`) |
| `data.broadcast` | boolean | `true` — the release was broadcast to the other replicas. Delivery is requested but not confirmed, and the broadcast does nothing when only one replica is active or the bus is down |
| `data.operation` | string | Type of the released operation: `"exec"` or `"deploy"`. Present only when `released: true` |
| `data.ageMs` | number | How many milliseconds the lock had been active. Present only when `released: true` |
| `data.expiresInMs` | number | In how many milliseconds the lock would have expired on its own. Present only when `released: true` |
| `data.message` | string | A note that there was no local lock and the release was broadcast to the replicas. Present only when `released: false` |

## Response example

A lock was held on this replica — released:

```json
{
  "success": true,
  "data": {
    "released": true,
    "localLock": true,
    "broadcast": true,
    "operation": "deploy",
    "ageMs": 320000,
    "expiresInMs": 580000
  }
}
```

No local lock (the release was broadcast to other replicas):

```json
{
  "success": true,
  "data": {
    "released": false,
    "localLock": false,
    "broadcast": true,
    "message": "No lock on this replica; a fleet-wide EXEC-lock release was broadcast to peers"
  }
}
```

## Error response example

404 — server not found:

```json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Server not found"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 401 | `MISSING_API_KEY` | No API key was supplied through `X-Api-Key` or a supported `Authorization: Bearer` scheme |
| 401 | `INVALID_API_KEY` | The authentication key is not recognized |
| 401 | `KEY_INACTIVE` | The requesting API key is no longer active |
| 401 | `KEY_EXPIRED` | The requesting API key has expired |
| 401 | `NO_PORTAL` | A non-management key has no portal binding |
| 401 | `COWORK_CHAT_KEY_INACTIVE` | The parent device key behind a Cowork chat key is no longer usable |
| 401 | `WRONG_AUTH_SCHEME` | An OAuth application key was sent as a Bearer session: put the key in `X-Api-Key` and the session in `Authorization: Bearer` |
| 401 | `INVALID_SESSION` | The supplied OAuth session is invalid or expired |
| 401 | `TOKEN_EXPIRED` | The OAuth user token expired and its refresh failed; re-authorization is required |
| 401 | `TOKEN_INVALID` | When the partner AI plane is enabled, its supplied token is unknown or expired |
| 401 | `KEY_REVOKED` | The partner credential behind the AI plane token is revoked |
| 402 | `ACCOUNT_FROZEN` | The key owner’s account is frozen. With wallet-debt scoping disabled, the shared API-key guard refuses this call before its handler. Follow error.topupUrl to resolve the balance; this status alone does not authorize repeating a prior write. |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork key — such a key works with data only and cannot perform write operations. To issue a key that can, see [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `PORTAL_COLLABORATOR_KEY_WRONG_SERVER` | The development-team key is bound to another server |
| 403 | `PORTAL_COLLABORATOR_NOT_A_MEMBER` | Development-team membership ended or the current role no longer permits this operation |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | A read-only key cannot make this write; for lock release this applies to MANAGEMENT keys despite the exemption for APP and chat keys. |
| 403 | `EXTERNAL_COLLABORATOR_KEY_OUT_OF_SCOPE` | This operation is outside the external-collaborator key route list. Use the key that manages the server |
| 403 | `IP_NOT_ALLOWED` | The caller IP is outside the key allowlist. |
| 403 | `OWNER_BLOCKED` | The key owner is blocked. |
| 403 | `OWNER_DELETED` | The key owner was deleted. |
| 403 | `PORTAL_SUSPENDED` | The bound portal is suspended. |
| 403 | `PORTAL_DELETED` | The bound portal was deleted. |
| 403 | `PORTAL_BLOCKED` | The bound portal is blocked. |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | The management key lacks the portal binding or entity authority required by this call. |
| 403 | `AGENT_MAINTENANCE_KEY_OUT_OF_SCOPE` | The maintenance key route list does not include this operation. |
| 403 | `APP_API_KEY_OUT_OF_SCOPE` | The application key route list does not include this operation. |
| 403 | `WAITLIST_PENDING` | The key owner has not been activated. |
| 403 | `SESSION_APP_MISMATCH` | The OAuth session belongs to another application. |
| 403 | `PLANE_FORBIDDEN` | The partner key does not allow this AI plane. |
| 403 | `SYSTEM_FORBIDDEN` | The partner key does not allow this AI system. |
| 403 | `SCOPE_FORBIDDEN` | The partner AI plane forbids this route. |
| 403 | `CONNECT_COMPANY_ROUTE_FORBIDDEN` | A company connection key does not allow this route. |
| 404 | `NOT_FOUND` | The server does not exist or belongs to another API key. An exception is a development-team key with a current `ADMIN` or `DEVELOPER` role on this server. |
| 409 | `AGENT_WRITE_ALREADY_EXECUTED` | With the agent-write replay guard enabled, this exact DELETE by the runtime agent key already has a claim and was not repeated. `error.firstExecutedAt` is the first claim timestamp; `error.firstStatus` is its HTTP status or `null` while the first attempt is still in flight. Reconcile and report that result; do not automatically repeat an unknown outcome. Failed first attempts release their claims |
| 429 | `QUOTA_EXCEEDED` | API billing is enabled with a nonzero overage price: the daily free quota is exhausted and the PREPAY balance is not positive. Top up the balance. `error.quota` and `error.used` report counts; `X-RateLimit-Quota`, `X-RateLimit-Used` and `X-RateLimit-Remaining` report the quota, usage and zero remainder. This refusal does not promise `Retry-After` or `X-RateLimit-Scope` |
| 429 | `CALLER_RATE_LIMITED` | The shared key or user request limit. Wait the seconds in `Retry-After`; `X-RateLimit-Scope` identifies the refusing layer |
| 429 | `RATE_LIMITED` | Lock-release limit: 6 requests per minute per key–server pair. The limit is divided across backend replicas; `x-ratelimit-limit` reports the effective ceiling and `Retry-After` reports the waiting time |
| 503 | `ACCOUNT_PENDING_ERASURE` | The key owner is pending erasure or being erased. Header `Retry-After: 3600`. |
| 503 | `SERVICE_DISABLED` | The partner AI plane or its quota-readiness service is disabled. No `Retry-After` is promised for this refusal. |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **One lock per server.** Protection against races between `/exec` and `/deploy` — they share one common lock. It is impossible to lock an individual command.
- **Auto-release on normal completion.** `/exec` and `/deploy` release the lock themselves, whether they succeed or fail. `DELETE /lock` is only needed when the client aborted the connection before completion or the agent timed out without sending a final event.
- **Platform lock TTL — auto-expiry without a call.** Synchronous `/exec` sets the lock for `timeout + 30 + 60` seconds. `/deploy` — for 15 minutes. Even without `DELETE /lock`, a stuck lock will expire on its own.
- **The release is broadcast to all replicas.** The platform scales horizontally, and a stuck lock is held on the replica that took it, which is not necessarily the one serving your `DELETE /lock`. So the release is always broadcast fleet-wide (`broadcast: true`) — that is how the endpoint releases the lock on the holder replica. That is also why `released` can be `false` even when the lock really was released: the local replica did not hold it.
- **Guaranteed auto-release of a stuck `exec` lock.** Even without `DELETE /lock`, a stuck `exec` lock is guaranteed to be released by a server-side background sweep shortly after its TTL expires (on the order of a few minutes) — for the case where the handler could not release it itself. `DELETE /lock` is the fast path; the sweep is the safety net. (There is no sweep for `/deploy`: a long deploy may legitimately exceed its TTL; a stuck deploy lock is released by `DELETE /lock` or by the operation finishing.)
- **`released: false` is a success**, not an error. The endpoint does not return 404 when there is no lock, because the idea is to "guarantee there is no lock". The client does not care whether there was one or not.
- **Works in any server mode.** Unlike most Deploy API endpoints, `DELETE /lock` does not require BLACKHOLE — the lock is held in the platform's memory and does not depend on the agent.
- **The lock outlives server deletion.** A stuck lock survives its server: if the previous server was deleted but its in-memory lock remained, the next deploy fails with `EXEC_BUSY`. `DELETE /lock` releases such a lock if the deleted server record and its binding to the managing API key are retained. Only the managing key can use this recovery path. A development-team key receives `403 PORTAL_COLLABORATOR_NOT_A_MEMBER` after server deletion, even if the key is still active and its membership record remains.

## See also

- [Run a command](./exec.md)
- [Full deploy](./deploy.md)
- [Tunnel metrics](./metrics.md)
