One task can now hold several concurrency limits at once. Set { perKey: 1, total: 10 } and every tenant runs one at a time while the task as a whole never passes 10. Add a named limit and that same run also draws from a pool shared across tasks. A run only starts when every limit it holds has room, and takes a slot in each while it runs.
Per-tenant and total, on one task
perKey caps each concurrencyKey pool. total caps every run of the task together, keyed or not. Set both and each tenant gets at most perKey while the whole task never passes total:
import { task } from "@trigger.dev/sdk";export const generateReport = task({ id: "generate-report", // each user runs at most 1 at a time, and at most 10 run in total concurrency: { perKey: 1, total: 10 }, run: async (payload) => { // ... },});
Share a limit across tasks
Declare a named limit with concurrencyLimit() and every task that holds it draws from the same pool. Handy when 3 different tasks all call the same rate-limited API:
import { concurrencyLimit, task } from "@trigger.dev/sdk";// at most 25 concurrent runs across every task that holds this limitexport const openaiLimit = concurrencyLimit({ name: "openai", total: 25 });export const summarizeThread = task({ id: "summarize-thread", // this task runs at most 5 at once, and also counts towards the openai limit concurrency: [{ total: 5 }, openaiLimit], run: async (payload) => { // ... },});
A task can hold one inline limit plus up to 2 named limits. A run only starts when every limit it holds has room, and it takes a slot in each while it executes.
Cap a tenant across every task
A named limit's perKey follows each run's own concurrencyKey. So one declaration caps a tenant across everything they trigger, while each task keeps its own tighter cap:
import { concurrencyLimit, task } from "@trigger.dev/sdk";// each tenant runs at most 10 at once across every task that holds this limitexport const tenantLimit = concurrencyLimit({ name: "tenant", perKey: 10 });export const processWebhook = task({ id: "process-webhook", // webhooks are capped at 2 per tenant, within the tenant's overall 10 concurrency: [{ perKey: 2 }, tenantLimit], run: async (payload) => { // ... },});await processWebhook.trigger(payload, { concurrencyKey: tenantId });
Switch limits when you trigger
Pass concurrency at trigger time to swap a run's named limits. The task's inline limit still applies:
// this run counts towards "priority" instead of the task's declared named limitsawait generateReport.trigger(data, { concurrency: ["priority"] });
Turn the dial without a deploy
The new concurrencyLimits namespace reads and changes limits at runtime. retrieve() returns each bound plus live running and queued counts:
import { concurrencyLimits } from "@trigger.dev/sdk";await concurrencyLimits.override("openai", { total: 50 });await concurrencyLimits.reset("openai"); // back to the values in your codeawait concurrencyLimits.pause("openai"); // nothing holding it can startawait concurrencyLimits.resume("openai");
Overrides survive deploys until you reset them. Pausing keeps your configured bounds, so resuming puts everything back as it was. The same controls are on the Concurrency page in the dashboard and in the management API.
Upgrading
Everything here needs @trigger.dev/sdk 4.7.0 or later. The queue-level concurrencyLimit option keeps working, and is now deprecated in favor of concurrency.
queues.overrideConcurrencyLimit() and queues.resetConcurrencyLimit() are deprecated too: a task's inline limit is addressable as task/<task-id>, so concurrencyLimits.override("task/my-task", { total: 10 }) does the same job. Pausing a queue with queues.pause() is unchanged.
Every pattern, from per-tenant caps to a global cap on a shared API, is in the concurrency docs.
