On this page
TL;DR — Submit answers tell you how many of your runs are running and waiting, and your limit.
GET /v2/queueshows the same numbers live.GET /v2/queue/loggives them per minute or per hour,GET /v2/queue/refusalslists refused submits, andGET /v2/queue/episodeslists congestion episodes. Subscribe toqueue.congestion_startedandqueue.congestion_endedto switch to your fallback early.
You only ever see your own product's numbers. Test keys and live keys count apart.
Words
| Word | Meaning |
|---|---|
| Concurrency slots | How many runs of your product run at once. The default is 20; Wandura raises it per product on request. |
| Queue | Priority runs that were accepted and wait for a slot. It holds as many runs as you have slots. |
| Refusal | A submit answered 429 because your product was at its limits. The reason is product_busy (slots and queue full) or rate_limited (over the per-minute run limit, answered gateway_rate_limited). No run was made and nothing was charged. |
| Congestion episode | A stretch of time your product was in one kind of congestion: refusing, near_full or slow. It has a start and an end. |
A per-end-user daily cap (end_user_cap_reached) is not a refusal: it is about one person, not your product.
Headers on every submit
Answers of POST /v2/styles/{style}, POST /v2/runs and POST /v2/jobs carry three headers: accepted, replayed and refused submits alike.
| Header | Meaning |
|---|---|
X-Concurrency-Limit |
Your concurrency slots. Your queue holds as many again. |
X-Concurrency-Running |
Runs of your product using a slot right now. |
X-Concurrency-Waiting |
Priority runs of your product waiting for a slot. |
HTTP/1.1 202
X-Concurrency-Limit: 20
X-Concurrency-Running: 20
X-Concurrency-Waiting: 6When Running and Waiting both reach Limit, a new priority run is refused: 429 product_busy (Jobs API: PRODUCT_BUSY) with Retry-After: 5. Relax runs are never refused for this; they wait.
- A submit refused before it reaches your limits (a bad body, an unknown style, a bad key) has no such headers.
- Treat the headers as optional: if the numbers cannot be read for a moment, the answer comes without them.
Live queue
GET /v2/queue returns your product's numbers right now, for the product and mode of your key.
curl https://api.wandura.ai/v2/queue -H "Authorization: Bearer $WANDURA_KEY"{
"object": "queue_state",
"product": "prod_…",
"livemode": true,
"concurrency_limit": 20,
"running": 20,
"waiting": 6,
"queue_limit": 20,
"accepted_this_minute": 41,
"per_minute_limit": 60,
"refusals": { "last_15m": 0, "last_24h": 3 },
"at": "2026-10-15T09:30:12.000Z"
}| Field | Meaning |
|---|---|
concurrency_limit |
Your concurrency slots. |
running |
Runs using a slot now. |
waiting |
Priority runs waiting for a slot now. |
queue_limit |
How many priority runs may wait. The same number as your slots. |
per_minute_limit |
Runs per minute for gateway keys and the Jobs API. Over it: 429 gateway_rate_limited (Jobs API: RATE_LIMITED). |
accepted_this_minute |
Runs counted against that limit since the start of the current UTC minute. |
refusals |
Refused submits in the last 15 minutes and the last 24 hours. |
Queue log
GET /v2/queue/log returns your numbers over time, one bucket per minute or per hour, oldest first.
| Parameter | Meaning |
|---|---|
granularity |
minute (default) or hour. |
from |
Start, included. A time inside a bucket includes that bucket. Default: 24 hours before to. |
to |
End, excluded. Default: now. |
- A range spans at most 7 days in minutes and 90 days in hours. Minutes are kept 7 days, hours 90 days.
- Buckets with no activity are left out.
- A minute shows up about a minute after it ends. The current hour fills as it goes.
- A wrong value is
400 invalid_request, and the message says which.
curl "https://api.wandura.ai/v2/queue/log?granularity=minute&from=2026-10-15T09:00:00Z" \
-H "Authorization: Bearer $WANDURA_KEY"{
"object": "list",
"product": "prod_…",
"livemode": true,
"granularity": "minute",
"from": "2026-10-15T09:00:00.000Z",
"to": "2026-10-15T09:30:00.000Z",
"data": [
{
"object": "queue_log_bucket",
"bucket": "2026-10-15T09:29:00.000Z",
"accepted": 38,
"refusals": { "product_busy": 2, "rate_limited": 0 },
"max_running": 20,
"max_waiting": 20,
"concurrency_limit": 20,
"ended": 17,
"median_wait_ms": 41000,
"median_duration_ms": 96000
}
]
}| Field | Meaning |
|---|---|
bucket |
Start of the minute or hour (UTC). |
accepted |
Runs accepted. |
refusals |
Refused submits, by reason. |
max_running |
The most runs using a slot at once. |
max_waiting |
The most priority runs waiting for a slot at once. |
concurrency_limit |
Your concurrency slots. |
ended |
Runs that ended after running. A run canceled while queued is not counted. |
median_wait_ms |
Median wait from submit to start, for the priority runs that started in the bucket. |
median_duration_ms |
Median run time, start to end (the wait not included), for the runs that ended in the bucket. |
Refusals
GET /v2/queue/refusals lists your refused submits, newest first. They are kept 30 days.
| Parameter | Meaning |
|---|---|
limit |
1 to 200. Default 50. |
before |
The id of the last item of the previous page. |
{
"object": "list",
"data": [
{
"object": "queue_refusal",
"id": "5821",
"at": "2026-10-15T09:29:41.204Z",
"reason": "product_busy",
"style": "sty_…",
"end_user_id": "user_42",
"retry_after_s": 5,
"livemode": true
}
],
"has_more": false
}reason is product_busy (slots and queue full) or rate_limited (the per-minute run limit). end_user_id is null when the call sent none. Use it to find which of your users were affected.
Congestion episodes
Wandura checks your product every minute. Each kind of congestion opens its own episode, so several can be open at once.
kind |
Starts when | Ends when |
|---|---|---|
refusing |
One or more refusals in the last 5 minutes. | 10 minutes with no refusal. |
near_full |
80% or more of your concurrency slots are in use for 1 minute. | Below 60% for 5 minutes. |
slow |
At least 3 runs ended in the last 15 minutes, and their median run time is over 10 minutes. | The median is back under 8 minutes, or nothing ended in 15 minutes and nothing runs or waits. |
- The same kind does not start again within 30 minutes of its end.
- Live keys only: test keys never start an episode.
- Run time is from start to end. The wait before it is not included.
GET /v2/queue/episodes lists your episodes, newest first. It takes the same limit and before as refusals.
{
"object": "list",
"data": [
{
"object": "congestion_episode",
"id": "184",
"product": "prod_…",
"livemode": true,
"kind": "refusing",
"status": "ended",
"started_at": "2026-10-15T09:31:00.000Z",
"ended_at": "2026-10-15T09:52:00.000Z",
"duration_s": 1260,
"start": { "running": 20, "waiting": 20, "concurrency_limit": 20, "refusals_5m": 3, "refusals_10m": 3, "runs_ended_15m": 24, "median_duration_ms": 95000 },
"end": { "running": 9, "waiting": 0, "concurrency_limit": 20, "refusals_5m": 0, "refusals_10m": 0, "runs_ended_15m": 30, "median_duration_ms": 88000 },
"affected": { "refusals": 7, "runs_accepted": 212, "runs_ended": 205 }
}
],
"has_more": false
}| Field | Meaning |
|---|---|
status |
open or ended. |
started_at, ended_at, duration_s |
When it started and ended. The last two are null while it is open. |
start, end |
Your numbers at the start and at the end (end is null while open): running, waiting, concurrency_limit, refusals in the last 5 and 10 minutes, runs ended in the last 15 minutes and their median run time (null when none ended). |
affected |
Once ended: refused submits (counted from 5 minutes before the start), runs accepted and runs ended during the episode. null while open. |
Webhook events
Each episode is sent twice to your product's webhook:
queue.congestion_startedwhen it starts.queue.congestion_endedwhen it ends.
They are opt-in: your webhook gets them only when its event list names them. To turn them on, ask Wandura to add both events to your product's webhook. Your other events stay as they are.
data is the episode as it is when the event is made. There is no sequence, because the event belongs to no run. Signature, retries, Wandura-Event-Id and Wandura-Event-Type work as for run events: see Webhooks.
{
"id": "evt_…",
"object": "event",
"type": "queue.congestion_started",
"created_at": "2026-10-15T09:31:00.412Z",
"data": {
"object": "congestion_episode",
"id": "184",
"product": "prod_…",
"livemode": true,
"kind": "refusing",
"status": "open",
"started_at": "2026-10-15T09:31:00.000Z",
"ended_at": null,
"duration_s": null,
"start": { "running": 20, "waiting": 20, "concurrency_limit": 20, "refusals_5m": 3, "refusals_10m": 3, "runs_ended_15m": 24, "median_duration_ms": 95000 },
"end": null,
"affected": null
}
}- Match a start and its end on
data.id. Rarely, an end can arrive before its start: trustdata.status. - The events are the fast signal. For the full record, read
GET /v2/queue/episodes.
What to do
- Watch the headers. When
X-Concurrency-Waitingclimbs towardX-Concurrency-Limit, slow down or send new work to your fallback before you are refused. - Act on the events. On
queue.congestion_started, switch to your fallback: at once forrefusing, early fornear_full, and forslowwarn your users or use a faster path. Onqueue.congestion_endedof the same kind, switch back. - On
429(product_busy,gateway_rate_limited; Jobs APIPRODUCT_BUSY,RATE_LIMITED): no run was made, so sending the request to your fallback cannot run it twice. To retry with Wandura, waitRetry-Afterseconds (add a little jitter) and resend with the sameIdempotency-Key. - On
503 service_unavailable(Jobs APISERVICE_UNAVAILABLE): nothing was created or charged. Retry afterRetry-Afterwith the sameIdempotency-Key. - Poll calmly. While your product is congested, runs wait longer. Poll each run every 5–10 s, or better, use webhooks. Polling faster does not start a run sooner.
- Need more room? The queue log and the refusals show when and how often you hit your limits. Send them to Wandura, and your product's slots or per-minute limit can be raised.
When Wandura itself has trouble, it is posted on wandura.instatus.com.
See also: Errors · Concepts: limits · Jobs API: limits.
Updated 15 Oct 2026 · Change history (1)