
Concurrency & Queues
Configure what you want to happen when there is more than one run at a time.
When you trigger a task, it isn’t executed immediately. Instead, the task run is placed into a queue for execution. By default, each task gets its own queue with unbounded concurrency—meaning the task runs as soon as resources are available, subject only to the overall concurrency limits of your environment. If you need more control (for example, to limit concurrency or share limits across multiple tasks), you can define a custom queue as described later in this document.
Controlling concurrency is useful when you have a task that can’t be run concurrently, or when you want to limit the number of runs to avoid overloading a resource.
Default concurrency
By default, all tasks have an unbounded concurrency limit, limited only by the overall concurrency limits of your environment. This means that each task could possibly “fill up” the entire concurrency limit of your environment.
Your environment has a maximum concurrency limit which depends on your plan. If you’re a paying customer you can request a higher limit by contacting us.
Setting task concurrency
You can set the concurrency limit for a task by setting the concurrencyLimit
property on the task’s queue. This limits the number of runs that can be executing at any one time:
This is useful if you need to control access to a shared resource, like a database or an API that has rate limits.
Sharing concurrency between tasks
As well as putting queue settings directly on a task, you can define a queue and reuse it across multiple tasks. This allows you to share the same concurrency limit:
In this example, task1
and task2
share the same queue, so only one of them can run at a time.
Setting the concurrency when you trigger a run
When you trigger a task you can override the concurrency limit. This is really useful if you sometimes have high priority runs.
The task:
Triggering from your backend and overriding the concurrency:
Concurrency keys and per-tenant queuing
If you’re building an application where you want to run tasks for your users, you might want a separate queue for each of your users (or orgs, projects, etc.).
You can do this by using concurrencyKey
. It creates a separate queue for each value of the key.
Your backend code:
Concurrency and subtasks
When you trigger a task that has subtasks, the subtasks will not inherit the concurrency settings of the parent task. Unless otherwise specified, subtasks will run on their own queue
Waits and concurrency
With our task checkpoint system, a parent task can trigger and wait for a subtask to complete. The way this system interacts with the concurrency system is a little complicated but important to understand. There are two main scenarios that we handle slightly differently:
- When a parent task waits for a subtask on a different queue.
- When a parent task waits for a subtask on the same queue.
These scenarios are discussed in more detail below:
We sometimes refer to the parent task as the “parent” and the subtask as the “child”. Subtask and child task are used interchangeably. We apologize for the confusion.
Waiting for a subtask on a different queue
During the time when a parent task is waiting on a subtask, the “concurrency” slot of the parent task is still considered occupied on the parent task queue, but is temporarily “released” to the environment. An example will help illustrate this:
For example purposes, let’s say the environment concurrency limit is 1. When the parent task is triggered, it will occupy the only slot in the environment. When the parent task triggers the subtask, the subtask will be placed in the queue for the subtask. The parent task will then wait for the subtask to complete. During this time, the parent task slot is temporarily released to the environment, allowing another task to run. Once the subtask completes, the parent task slot is reoccupied.
This system prevents “stuck” tasks. If the parent task were to wait on the subtask and not release the slot, the environment would be stuck with only one task running.
And because only the environment slot is released, the parent task queue slot is still occupied. This means that if another task is triggered on the parent task queue, it will be placed in the queue and wait for the parent task to complete, respecting the concurrency limit.
Waiting for a subtask on the same queue
Because tasks can trigger and wait recursively, or share the same queue, we’ve added special handling for when a parent task waits for a subtask on the same queue.
Recall above that when waiting for a subtask on a different queue, the parent task slot is temporarily released to the environment. When the parent task and the subtask share a queue, we also release the parent task slot to the queue. Again, an example will help illustrate this:
In this example, the parent task and the subtask share the same queue with a concurrency limit of 1. When the parent task triggers the subtask, the parent task slot is released to the queue, giving the subtask the opportunity to run. Once the subtask completes, the parent task slot is reoccupied.
It’s very important to note that we only release at-most X slots to the queue, where X is the concurrency limit of the queue. This means that you can only trigger and wait for X subtasks on the same queue. If you try to trigger and wait for more than X subtasks, you will receive a RECURSIVE_WAIT_DEADLOCK
error. The following example will result in a deadlock:
Now this will result in a RECURSIVE_WAIT_DEADLOCK
error because the parent task is waiting for the subtask, and the subtask is waiting for the subsubtask, but there is no more concurrency available in the queue. It will look a bit like this in the logs:
Mitigating recursive wait deadlocks
If you are recursively triggering and waiting for tasks on the same queue, you can mitigate the risk of a deadlock by increasing the concurrency limit of the queue. This will allow you to trigger and wait for more subtasks.
You can also use different queues for the parent task and the subtask. This will allow you to trigger and wait for more subtasks without the risk of a deadlock.