Async

Introduction

Pools can accept work asynchronously. The caller gets 202 Accepted with a job id and poll URL. The job still competes for the pool's concurrency limit. When the upstream finishes, nthbouncer posts a signed webhook and the poll endpoint returns the result.

Async needs at least one active webhook destination. Enable async on Pools → Concurrency, then attach a destination from Webhooks. Sync proxy stays the default. Team and Business plans only.

Admission modes

Mode How it triggers reason on poll / webhook
Header X-Nthpool-Async: 1 on a normal pool request requested
On open wait Request would wait for a slot → accept as a job instead of holding the connection busy
On queue timeout Open wait hits queue timeout → accept as a job instead of 504 queue_timeout wait_timeout

All three share one job pipeline. Slot timing: the concurrency slot is held only while the upstream fetch runs (not while the job waits in the queue).

Async bypasses the shared response cache. Locks stay sync-only.

No active destination

A pool with async on but no active destination falls back to sync. That covers both cases: nothing attached yet, and every attached webhook deactivated after repeated delivery failures.

In that state the pool behaves exactly as it would with async off:

  • X-Nthpool-Async: 1 returns the upstream response, not 202.
  • On open wait and on queue timeout do not promote. A request waits for a slot as usual and gets 429 or 504 queue_timeout on its own terms.

No jobs are created while a pool is in this state, so nothing sits unclaimed. Attach a working destination (or re-enable a deactivated one) and the next request goes back through the async path.

A pool that never enabled async still rejects X-Nthpool-Async: 1 with 400 async_disabled.

Accept response

{
  "jobId": "job_…",
  "status": "accepted",
  "pollUrl": "https://api.nthpool.cloud/pools/your-pool/jobs/job_…",
  "webhook": { "destinations": 2 }
}

pollUrl is https://api.nthpool.cloud/pools/{pool}/jobs/{jobId}, the same API host as concurrency locks, not your pool hostname or a custom domain. {pool} is your pool subdomain. Job ids are job_ followed by hex. Database pools are the exception: their handles poll on the pool's own hostname — see Database pools.

Poll

GET (or HEAD) the pollUrl with the same pool auth as a normal request or a lock call. Poll is there for callers that cannot receive a webhook. When destinations are configured, the webhook remains the primary completion signal.

Accept is 202 with status: "accepted". Poll always returns 200 for a known job, with status set to the current lifecycle value:

acceptedqueuedrunning → terminal completed | failed | expired

There is no job-level pending.

Case HTTP Body
Unknown or wrong-pool job 404 { "error": "job_not_found" }
Past expiresAt, still non-terminal 200 status: "expired"
Terminal job still in store 200 that terminal status

Every poll body includes jobId, status, reason, createdAt, updatedAt, and expiresAt. Terminal jobs may include result (upstream status, headers, size-capped body). When destinations are on the job, webhook reports destinations, delivered, and failed counts.

Retention

Job retention defaults to 86400 seconds (24 hours). Configurable per pool from 60 seconds to 604800 (7 days).

Webhooks

See Webhooks for destinations, signing, and retries.

Limitations

  • On proxy pools, async requires at least one active webhook destination; without one the pool serves sync. Database pools return handles with async enabled alone.
  • Max request body retained for a job defaults to 1 MiB (configurable per pool).
  • Job retention defaults to 24 hours (configurable from 60 seconds to 7 days).
  • Webhook delivery is at-least-once; receivers must be idempotent on jobId.
  • Destination count per pool is plan-capped.

Next steps