# Concurrency locks

## Base URL

Lock routes live only on
`https://api.nthpool.cloud/pools/{pool}/lock`, never on `‹pool›.nthpool.cloud`
or a custom domain. `{pool}` is your pool subdomain (for example `prod-acme`).
The Examples section on the pool Overview page in the dashboard includes
acquire, renew, and release.

All lock endpoints are **POST** and return JSON.

## Authentication

Same caller auth as the [pool](/docs/pools):

- Pool key (`X-Nthpool-Key`, or `Authorization: Bearer` / `Nthbouncer` when allowed)
- Optional HMAC, JWT, Cloudflare Access, and IP policies from
  [Access](/docs/pools/access)

The lock API never emits managed upstream credentials. You authenticate to the
pool for a slot, then call the upstream yourself with whatever auth the origin
expects.

## First request

```bash
export POOL_KEY='nb_live_…'
export LOCK_API='https://api.nthpool.cloud/pools/your-pool/lock'

curl -sS -X POST "$LOCK_API/acquire" \
  -H "X-Nthpool-Key: $POOL_KEY" \
  -H "Content-Type: application/json"
```

A `200` body looks like:

```json
{
  "slotId": "…",
  "leaseDeadline": 1710000030000,
  "requestDeadline": 1710000060000,
  "termMs": 30000,
  "renewIntervalMs": 10000
}
```

Hold `slotId` privately. Renew before `leaseDeadline` (about every
`renewIntervalMs`, typically 10s). Renewal never extends past `requestDeadline`
(the pool request/lease timeout). A vanished client's slot is reclaimed within
one term (`termMs`, typically 30s).

## Endpoints

### `POST /pools/{pool}/lock/acquire`

**Body (optional JSON):**

| Field | Meaning |
|-------|---------|
| `waitMs` | Opt-in queue wait in milliseconds (clamped to the pool queue timeout). Absent → fail fast |

You can also set wait with `X-Nthpool-Queue-Timeout-Ms`. The JSON body wins if
both are present. `X-Nthpool-No-Queue` forces fail-fast even when `waitMs` is set.

**Success `200`:** [Lock grant](#first-request) fields above, plus the usual
governor response headers (`x-nthpool-remaining-slots`,
`x-nthpool-effective-limit`, `x-nthpool-queued-ms`, …).

**Errors:**

| Status | `error` | When |
|--------|---------|------|
| `401` | `unauthorized` | Missing/invalid auth or nonce replay |
| `403` | `unauthorized` | IP allowlist miss |
| `413` | `payload_too_large` | Body too large |
| `429` | `too_many_requests` | No free slot (fail-fast or queue full). May include `Retry-After` |
| `504` | `queue_timeout` | Waited `waitMs` (or header budget) and no slot opened |

Acquire attempts are written to the pool access log. Renew and release are not.

### `POST /pools/{pool}/lock/{slotId}/renew`

Empty body. Call on a timer while your upstream work runs (every
`renewIntervalMs`).

| Status | Body | When |
|--------|------|------|
| `200` | `{ "leaseDeadline": … }` | Term extended |
| `404` | `not_found` | Slot already released or reclaimed |
| `409` | `capped` | Slot hit the request-timeout cap. Release it. |

### `POST /pools/{pool}/lock/{slotId}/release`

**Body (optional JSON).** Outcome report that feeds adaptive concurrency / RPM
learning (same rate-header dialects as the proxy):

| Field | Meaning |
|-------|---------|
| `status` | Final upstream HTTP status (100-599) |
| `throttled` | Count of upstream `429`s you observed |
| `retryAfterMs` | Upstream retry hint in milliseconds |
| `rate` | Pre-parsed rate fields (`limit`, `remaining`, `resetMs`, `tokenLimit`, …) |
| `headers` | Raw upstream response headers to parse (`x-ratelimit-*`, etc.) |

If both `rate` and `headers` are sent, `rate` wins.

**Success `200`:** `{ "released": true }` when the slot was held, or
`{ "released": false }` if it was already gone (idempotent).

```bash
curl -sS -X POST "$LOCK_API/$SLOT_ID/release" \
  -H "X-Nthpool-Key: $POOL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":200}'
```

Throttle report example:

```bash
curl -sS -X POST "$LOCK_API/$SLOT_ID/release" \
  -H "X-Nthpool-Key: $POOL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":429,"throttled":1,"retryAfterMs":2000}'
```

## Lifecycle

1. **Acquire** a slot.
2. Call the upstream from your process.
3. **Renew** on the recommended interval while the call runs.
4. **Release** when finished (include outcome fields when you saw throttling).

Acquire shares the pool's concurrency limit, queue, and RPM gate with proxy
traffic. See [Concurrency](/docs/pools/concurrency) for those settings.

## See also

- [Acquire a concurrency lock without proxying](/docs/knowledge-base/concurrency-lock) (cookbook)
- [Pools](/docs/pools) (when nthbouncer should call the upstream for you)
- [Admission errors](/docs/knowledge-base/admission-errors)
