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:
- Pool key (
X-Nthpool-Key, orAuthorization: Bearer/Nthbouncerwhen allowed) - Optional HMAC, JWT, Cloudflare Access, and IP policies from 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
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:
{
"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 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 429s 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).
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:
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
- Acquire a slot.
- Call the upstream from your process.
- Renew on the recommended interval while the call runs.
- 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 for those settings.
See also
- Acquire a concurrency lock without proxying (cookbook)
- Pools (when nthbouncer should call the upstream for you)
- Admission errors