Skip to main content
When you trigger a task, the run is placed into a queue. Trigger.dev then picks runs off the queue and executes them according to the concurrency limits you configure. By default each task gets its own queue with no concurrency limit other than the overall limit for your environment. Only actively executing runs count towards concurrency — runs that are waiting at a waitpoint release their slot until they resume.

Default concurrency

All tasks start with an unbounded concurrency limit, capped by your environment’s overall limit.
Environments have a base concurrency limit and a burstable limit (default burst factor: 2x the base limit). Individual queues are limited by the base limit, not the burstable limit. For example, if your base limit is 10, the environment can burst to 20 concurrent runs, but any single queue can have at most 10 concurrent runs at once.

Setting task concurrency

Limit how many runs of a task can execute simultaneously using queue.concurrencyLimit:
/trigger/one-at-a-time.ts
import { task } from "@trigger.dev/sdk";

export const oneAtATime = task({
  id: "one-at-a-time",
  queue: {
    concurrencyLimit: 1,
  },
  run: async (payload: any) => {
    // Only one run of this task will execute at a time
  },
});
This is useful when you need to control access to a shared resource like a database or an API with rate limits.

Sharing concurrency between tasks

Define a named queue and assign it to multiple tasks. All tasks sharing the queue will respect the same concurrency limit:
/trigger/queue.ts
import { task, queue } from "@trigger.dev/sdk";

export const myQueue = queue({
  name: "my-queue",
  concurrencyLimit: 1,
});

export const task1 = task({
  id: "task-1",
  queue: myQueue,
  run: async (payload: { message: string }) => {
    // ...
  },
});

export const task2 = task({
  id: "task-2",
  queue: myQueue,
  run: async (payload: { message: string }) => {
    // ...
  },
});
In this example, task1 and task2 share the same queue, so only one of them can run at a time across both tasks.

Overriding the queue at trigger time

You can override the default queue when triggering a run. This is useful for high-priority runs that need more concurrency:
/trigger/override-concurrency.ts
import { task, queue } from "@trigger.dev/sdk";

const paidQueue = queue({
  name: "paid-users",
  concurrencyLimit: 10,
});

export const generatePullRequest = task({
  id: "generate-pull-request",
  queue: {
    concurrencyLimit: 1,
  },
  run: async (payload: any) => {
    // ...
  },
});
app/api/push/route.ts
import { generatePullRequest } from "~/trigger/override-concurrency";

export async function POST(request: Request) {
  const data = await request.json();

  if (data.branch === "main") {
    // Use the high-concurrency paid-users queue for main branch
    const handle = await generatePullRequest.trigger(data, {
      queue: "paid-users",
    });
    return Response.json(handle);
  } else {
    // Use the default queue (concurrency of 1)
    const handle = await generatePullRequest.trigger(data);
    return Response.json(handle);
  }
}

Per-tenant queuing with concurrencyKey

Use concurrencyKey to create a separate queue for each unique value. This is useful when you want isolated concurrency limits per user, organization, or any other entity:
app/api/pr/route.ts
import { generatePullRequest } from "~/trigger/override-concurrency";

export async function POST(request: Request) {
  const data = await request.json();

  if (data.isFreeUser) {
    const handle = await generatePullRequest.trigger(data, {
      queue: { name: "free-users", concurrencyLimit: 1 },
      // Creates a separate "free-users" queue per userId
      concurrencyKey: data.userId,
    });
    return Response.json(handle);
  } else {
    const handle = await generatePullRequest.trigger(data, {
      queue: { name: "paid-users", concurrencyLimit: 10 },
      // Creates a separate "paid-users" queue per userId
      concurrencyKey: data.userId,
    });
    return Response.json(handle);
  }
}

Concurrency and waits

When a run reaches a waitpoint (such as wait.for(), triggerAndWait(), or wait.forToken()), it is checkpointed and transitions to a WAITING state. At this point, the run releases its concurrency slot back to the queue and environment. This means:
  • Only actively executing runs count towards concurrency limits.
  • Runs in the WAITING state do not hold a slot — you can have far more waiting runs than your concurrency limit.
  • When a waiting run resumes, it must re-acquire a slot before continuing.

Parent and subtask concurrency

When a parent task triggers and waits for a subtask on a different queue, the parent checkpoints and releases its slot. This prevents deadlocks:
/trigger/waiting.ts
import { task } from "@trigger.dev/sdk";

export const parentTask = task({
  id: "parent-task",
  queue: {
    concurrencyLimit: 1,
  },
  run: async (payload: any) => {
    // The parent checkpoints here, releasing its slot
    // so other runs in the queue can proceed
    await subtask.triggerAndWait(payload);
    // Resumes after subtask completes and re-acquires a slot
  },
});

export const subtask = task({
  id: "subtask",
  run: async (payload: any) => {
    // ...
  },
});

Managing queues with the SDK

Import the queues namespace to manage queues programmatically:
import { queues } from "@trigger.dev/sdk";

Listing queues

import { queues } from "@trigger.dev/sdk";

const allQueues = await queues.list();

// With pagination
const pagedQueues = await queues.list({ page: 1, perPage: 20 });

Retrieving a queue

import { queues } from "@trigger.dev/sdk";

// By queue ID
const queueById = await queues.retrieve("queue_1234");

// By type and name
const taskQueue = await queues.retrieve({ type: "task", name: "my-task-id" });
const customQueue = await queues.retrieve({ type: "custom", name: "my-custom-queue" });
The returned queue object includes:
{
  id: "queue_1234",
  name: "my-task-id",
  type: "task",        // "task" or "custom"
  running: 5,          // Currently executing runs
  queued: 10,          // Runs waiting to execute
  paused: false,       // Whether the queue is paused
  concurrencyLimit: 10,
  concurrency: {
    current: 10,       // Effective limit
    base: 10,          // Default from code
    override: null,    // Override value (if set)
  }
}

Pausing and resuming queues

Pausing a queue prevents new runs from starting. Currently executing runs continue to completion.
import { queues } from "@trigger.dev/sdk";

// Pause
await queues.pause("queue_1234");
await queues.pause({ type: "task", name: "my-task-id" });

// Resume
await queues.resume("queue_1234");
await queues.resume({ type: "custom", name: "my-custom-queue" });

Overriding concurrency limits

Temporarily change a queue’s concurrency limit at runtime:
import { queues } from "@trigger.dev/sdk";

// Set a new limit
await queues.overrideConcurrencyLimit("queue_1234", 5);
await queues.overrideConcurrencyLimit({ type: "task", name: "my-task-id" }, 20);

// Reset to the value defined in code
await queues.resetConcurrencyLimit("queue_1234");
await queues.resetConcurrencyLimit({ type: "task", name: "my-task-id" });

Build docs developers (and LLMs) love