Skip to content

Commit e2d8a5f

Browse files
committed
docs: combined concurrency limits and queue gates
1 parent 1beeb54 commit e2d8a5f

5 files changed

Lines changed: 264 additions & 1 deletion

File tree

docs/docs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,9 @@
379379
"management/queues/retrieve",
380380
"management/queues/pause",
381381
"management/queues/concurrency-override",
382-
"management/queues/concurrency-reset"
382+
"management/queues/concurrency-reset",
383+
"management/queues/combined-concurrency-override",
384+
"management/queues/combined-concurrency-reset"
383385
]
384386
},
385387
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Override Combined Concurrency Limit"
3+
openapi: "v3-openapi POST /api/v1/queues/{queueParam}/concurrency/combined/override"
4+
---
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: "Reset Combined Concurrency Limit"
3+
openapi: "v3-openapi POST /api/v1/queues/{queueParam}/concurrency/combined/reset"
4+
---

docs/queue-concurrency.mdx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,86 @@ export async function POST(request: Request) {
157157
}
158158
```
159159

160+
## Combined concurrency across keys
161+
162+
`concurrencyKey` gives every key value its own copy of the queue, each with the queue's full `concurrencyLimit`. That means the queue's total concurrency grows with the number of active keys: ten active users on a queue with `concurrencyLimit: 5` can run 50 at once.
163+
164+
To bound the whole queue, set `combinedConcurrencyLimit`. Each key still gets at most `concurrencyLimit`, and the queue as a whole never exceeds the combined limit across all keys:
165+
166+
```ts /trigger/per-user.ts
167+
export const perUserQueue = queue({
168+
name: "per-user-queue",
169+
//each user runs at most 1 at a time...
170+
concurrencyLimit: 1,
171+
//...and at most 10 users can be running at once
172+
combinedConcurrencyLimit: 10,
173+
});
174+
```
175+
176+
The combined limit only applies to runs triggered with a `concurrencyKey`; runs without a key are governed by `concurrencyLimit` alone. On the Queues page in the dashboard, a queue with a combined limit shows it in brackets next to the per-key limit, e.g. `1 (10)`.
177+
178+
<Note>
179+
If you self-host, combined limits are enforced by default and can be disabled with
180+
`RUN_ENGINE_TOTAL_CONCURRENCY_LIMITS_ENABLED=0`. When enforcement is disabled the limit is
181+
still accepted, stored, and shown, but runs are not held back by it.
182+
</Note>
183+
184+
## Holding slots in more than one queue (queue gates)
185+
186+
Sometimes one limit isn't enough: a webhook processor should be capped as a task, but each tenant should also have a global cap across every task they run. Queue gates let a run hold a concurrency slot in more than one queue at once.
187+
188+
Pass an array as `queue`: the first entry is the run's home queue (where it waits), and up to two more entries name gates — other queues the run must also have capacity in before it starts, and occupies while it executes:
189+
190+
```ts /trigger/webhooks.ts
191+
export const tenantQueue = queue({ name: "tenant", concurrencyLimit: 10 });
192+
193+
export const processWebhook = task({
194+
id: "process-webhook",
195+
queue: [{ name: "webhooks", concurrencyLimit: 2 }, "tenant"],
196+
run: async (payload) => {
197+
//...
198+
},
199+
});
200+
```
201+
202+
```ts app/api/webhook/route.ts
203+
//the run waits in "webhooks" and also counts towards this tenant's cap
204+
await processWebhook.trigger(payload, { concurrencyKey: tenantId });
205+
```
206+
207+
A gate without a `concurrencyKey` uses the run's own key, so the shared `tenant` queue above caps each tenant across every task that names it as a gate. Give the gate a literal key to pin it to a single slot pool instead, for example capping all traffic to one external provider across your whole environment:
208+
209+
```ts /trigger/sync.ts
210+
export const syncToProvider = task({
211+
id: "sync-to-provider",
212+
queue: [
213+
{ name: "sync-home", concurrencyLimit: 20 },
214+
//every run shares one "provider-api" pool regardless of its own key
215+
{ name: "provider-api", concurrencyKey: "shared" },
216+
],
217+
run: async (payload) => {
218+
//...
219+
},
220+
});
221+
```
222+
223+
The same array form works when you trigger, replacing the task's gates for that run:
224+
225+
```ts
226+
await processWebhook.trigger(payload, {
227+
queue: ["webhooks", "tenant"],
228+
concurrencyKey: tenantId,
229+
});
230+
```
231+
232+
A run starts only when its home queue and every gate all have capacity, and it releases all of its slots together when it finishes or suspends.
233+
234+
<Note>
235+
Queue gates are enforced when the server has them enabled. If you self-host, set
236+
`RUN_ENGINE_QUEUE_GATES_ENABLED=1`; servers without gates enabled accept the option but run
237+
without it.
238+
</Note>
239+
160240
## Concurrency and subtasks
161241

162242
When you trigger a task that has subtasks, the subtasks will not inherit the queue from the parent task. Unless otherwise specified, subtasks will run on their own queue
@@ -356,3 +436,19 @@ await queues.resetConcurrencyLimit("queue_1234");
356436
// Or using type and name
357437
await queues.resetConcurrencyLimit({ type: "task", name: "my-task-id" });
358438
```
439+
440+
### Overriding the combined concurrency limit
441+
442+
Queues with a `combinedConcurrencyLimit` can have that cap overridden and reset in the same way:
443+
444+
```ts
445+
import { queues } from "@trigger.dev/sdk";
446+
447+
// Allow up to 100 runs across all concurrency keys
448+
await queues.overrideCombinedConcurrencyLimit("queue_1234", 100);
449+
450+
// Revert to the combinedConcurrencyLimit declared in your code
451+
await queues.resetCombinedConcurrencyLimit("queue_1234");
452+
```
453+
454+
Overrides survive deploys: redeploying your code keeps an active override until you reset it.

docs/v3-openapi.yaml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3048,6 +3048,132 @@ paths:
30483048
20
30493049
);
30503050
3051+
"/api/v1/queues/{queueParam}/concurrency/combined/override":
3052+
post:
3053+
operationId: override_queue_combined_concurrency_v1
3054+
summary: Override combined concurrency limit
3055+
description: |
3056+
Override the combined concurrency limit of a queue: the cap on concurrent runs across
3057+
all of the queue's `concurrencyKey` values. Useful for temporarily scaling a whole
3058+
keyed queue up or down without changing each key's own limit.
3059+
parameters:
3060+
- in: path
3061+
name: queueParam
3062+
required: true
3063+
schema:
3064+
type: string
3065+
description: The queue ID (e.g., `queue_1234`), or the name of the queue when using the `type` body parameter.
3066+
example: queue_1234
3067+
requestBody:
3068+
required: true
3069+
content:
3070+
application/json:
3071+
schema:
3072+
type: object
3073+
required: ["combinedConcurrencyLimit"]
3074+
properties:
3075+
type:
3076+
type: string
3077+
enum: [id, task, custom]
3078+
default: id
3079+
description: |
3080+
How to interpret the `queueParam` path parameter:
3081+
- `id`: Treat as a queue ID (default)
3082+
- `task`: Treat as a task ID to get the task's default queue
3083+
- `custom`: Treat as a custom queue name
3084+
combinedConcurrencyLimit:
3085+
type: integer
3086+
minimum: 0
3087+
maximum: 100000
3088+
description: |
3089+
The new combined concurrency limit to set for the queue. It may not exceed
3090+
your environment's maximum concurrency limit: a higher value is rejected
3091+
with a 400, not capped to the maximum.
3092+
responses:
3093+
"200":
3094+
description: Combined concurrency limit overridden successfully
3095+
content:
3096+
application/json:
3097+
schema:
3098+
"$ref": "#/components/schemas/QueueObject"
3099+
"400":
3100+
description: |
3101+
Invalid request parameters, or the requested combined concurrency limit exceeds
3102+
the environment's maximum concurrency limit.
3103+
"401":
3104+
description: Unauthorized request
3105+
"404":
3106+
description: Queue not found
3107+
tags:
3108+
- queues
3109+
security:
3110+
- secretKey: []
3111+
x-codeSamples:
3112+
- lang: typescript
3113+
source: |-
3114+
import { queues } from "@trigger.dev/sdk";
3115+
3116+
// Allow up to 100 runs across all concurrency keys
3117+
await queues.overrideCombinedConcurrencyLimit("queue_1234", 100);
3118+
3119+
// Using type and name
3120+
await queues.overrideCombinedConcurrencyLimit(
3121+
{ type: "custom", name: "per-user-queue" },
3122+
100
3123+
);
3124+
3125+
"/api/v1/queues/{queueParam}/concurrency/combined/reset":
3126+
post:
3127+
operationId: reset_queue_combined_concurrency_v1
3128+
summary: Reset combined concurrency limit
3129+
description: Reset the combined concurrency limit of a queue back to the `combinedConcurrencyLimit` declared in your code.
3130+
parameters:
3131+
- in: path
3132+
name: queueParam
3133+
required: true
3134+
schema:
3135+
type: string
3136+
description: The queue ID (e.g., `queue_1234`), or the name of the queue when using the `type` body parameter.
3137+
example: queue_1234
3138+
requestBody:
3139+
required: false
3140+
content:
3141+
application/json:
3142+
schema:
3143+
type: object
3144+
properties:
3145+
type:
3146+
type: string
3147+
enum: [id, task, custom]
3148+
default: id
3149+
description: |
3150+
How to interpret the `queueParam` path parameter:
3151+
- `id`: Treat as a queue ID (default)
3152+
- `task`: Treat as a task ID to get the task's default queue
3153+
- `custom`: Treat as a custom queue name
3154+
responses:
3155+
"200":
3156+
description: Combined concurrency limit reset successfully
3157+
content:
3158+
application/json:
3159+
schema:
3160+
"$ref": "#/components/schemas/QueueObject"
3161+
"401":
3162+
description: Unauthorized request
3163+
"404":
3164+
description: Queue not found
3165+
tags:
3166+
- queues
3167+
security:
3168+
- secretKey: []
3169+
x-codeSamples:
3170+
- lang: typescript
3171+
source: |-
3172+
import { queues } from "@trigger.dev/sdk";
3173+
3174+
// Revert to the combinedConcurrencyLimit declared in code
3175+
await queues.resetCombinedConcurrencyLimit("queue_1234");
3176+
30513177
"/api/v1/queues/{queueParam}/concurrency/reset":
30523178
post:
30533179
operationId: reset_queue_concurrency_v1
@@ -4321,6 +4447,37 @@ components:
43214447
format: date-time
43224448
nullable: true
43234449
description: When the concurrency limit was overridden
4450+
combined:
4451+
type: object
4452+
description: |
4453+
The combined concurrency cap across all `concurrencyKey` values of the queue.
4454+
Present when the queue has a `combinedConcurrencyLimit`.
4455+
properties:
4456+
current:
4457+
type: integer
4458+
nullable: true
4459+
description: The current combined concurrency limit as declared or overridden (null = no cap). Enforcement clamps it to the environment concurrency limit at admit time.
4460+
example: 10
4461+
base:
4462+
type: integer
4463+
nullable: true
4464+
description: The declared combined limit an override reverts to on reset
4465+
example: 10
4466+
override:
4467+
type: integer
4468+
nullable: true
4469+
description: The overridden combined limit, when an override is active
4470+
example: null
4471+
overriddenAt:
4472+
type: string
4473+
format: date-time
4474+
nullable: true
4475+
description: When the combined override was applied
4476+
running:
4477+
type: integer
4478+
nullable: true
4479+
description: Runs currently in flight across all concurrencyKey values
4480+
example: 4
43244481
example: null
43254482
overriddenBy:
43264483
type: string

0 commit comments

Comments
 (0)