From 0afc17eabf480f5c3e78e93262cd542dff9d7e3e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 28 Jul 2026 12:00:56 +0100 Subject: [PATCH] docs(wait): separate compute billing from concurrency release The wait pages described the 5 second compute-billing threshold as if it were also the suspension threshold. It isn't: `wait.for` and `wait.until` only checkpoint 60 seconds into the wait, so a shorter wait stays EXECUTING and holds its concurrency slot for the whole wait even though the compute is free. Splits the two facts in the shared wait snippet and corrects the same conflation on the concurrency, spend and how-it-works pages. --- docs/how-it-works.mdx | 6 +++--- docs/how-to-reduce-your-spend.mdx | 8 ++++---- docs/queue-concurrency.mdx | 4 ++++ docs/snippets/paused-execution-free.mdx | 4 +++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/how-it-works.mdx b/docs/how-it-works.mdx index 88bb4c07036..c7d3e089c1f 100644 --- a/docs/how-it-works.mdx +++ b/docs/how-it-works.mdx @@ -148,7 +148,7 @@ Here's how the Checkpoint-Resume System works: 2. **Subtask Handling**: If a task needs to trigger a subtask, it can do so and wait for its completion using `triggerAndWait` -3. **State Checkpointing**: While waiting for a subtask or during a programmed pause (e.g., `wait.for({ seconds: 30 })`), the system uses CRIU (Checkpoint/Restore In Userspace) to create a checkpoint of the task's entire state, including memory, CPU registers, and open file descriptors. +3. **State Checkpointing**: While waiting for a subtask or during a long programmed pause (e.g., `wait.for({ minutes: 5 })`), the system uses CRIU (Checkpoint/Restore In Userspace) to create a checkpoint of the task's entire state, including memory, CPU registers, and open file descriptors. A `wait.for()` or `wait.until()` shorter than 60 seconds doesn't checkpoint — see [Wait](/wait). 4. **Resource Release**: After checkpointing, the parent task's resources are released, freeing up the execution environment. @@ -178,9 +178,9 @@ export const parentTask = task({ console.log("Child task result:", result); // This will also cause the task to be checkpointed and suspended - await wait.for({ seconds: 30 }); + await wait.for({ minutes: 5 }); - console.log("Resumed after 30 seconds"); + console.log("Resumed after 5 minutes"); return "Parent task completed"; }, diff --git a/docs/how-to-reduce-your-spend.mdx b/docs/how-to-reduce-your-spend.mdx index 7376ffafe74..b19bdb8476b 100644 --- a/docs/how-to-reduce-your-spend.mdx +++ b/docs/how-to-reduce-your-spend.mdx @@ -170,7 +170,7 @@ export const boundedTask = task({ ## Use waitpoints instead of polling -Waits longer than 5 seconds automatically checkpoint your task, meaning you don't pay for compute while waiting. Use `wait.for()`, `wait.until()`, or `triggerAndWait()` instead of polling loops. +You don't pay for compute during any wait longer than 5 seconds. Use `wait.for()`, `wait.until()`, or `triggerAndWait()` instead of polling loops. ```ts import { task, wait } from "@trigger.dev/sdk"; @@ -178,17 +178,17 @@ import { task, wait } from "@trigger.dev/sdk"; export const waitpointTask = task({ id: "waitpoint-task", run: async (payload) => { - // This wait is free - your task is checkpointed + // You aren't charged for compute during this wait await wait.for({ minutes: 5 }); - // Parent is also checkpointed while waiting for child tasks + // The parent isn't charged either while it waits for a child task const result = await childTask.triggerAndWait({ data: payload }); return result; }, }); ``` -[Read more about waitpoints](/wait-for). +Waiting saves you compute, but it doesn't always free up concurrency — see [Wait](/wait). ## Use debounce to consolidate multiple triggers diff --git a/docs/queue-concurrency.mdx b/docs/queue-concurrency.mdx index 24e77f4e67f..b832ffc26da 100644 --- a/docs/queue-concurrency.mdx +++ b/docs/queue-concurrency.mdx @@ -199,6 +199,10 @@ For example, if you have a queue with a `concurrencyLimit` of 1: - When the executing run reaches a waitpoint and checkpoints, it releases its slot - The next queued run can then begin execution +### Short time-based waits keep their slot + +Checkpointing takes time, so a run doesn't checkpoint the moment it reaches a waitpoint. For [`wait.for()`](/wait-for) and [`wait.until()`](/wait-until) it happens 60 seconds into the wait, so anything shorter stays `EXECUTING` and holds its slot for the whole wait. If you're polling in a loop, use an interval comfortably above 60 seconds so the slot is actually released between polls. + ### Waiting for a subtask on a different queue When a parent task triggers and waits for a subtask on a different queue, the parent task will checkpoint and release its concurrency slot once it reaches the wait point. This prevents environment deadlocks where all concurrency slots would be occupied by waiting tasks. diff --git a/docs/snippets/paused-execution-free.mdx b/docs/snippets/paused-execution-free.mdx index 7a9483d451e..6372d1f783b 100644 --- a/docs/snippets/paused-execution-free.mdx +++ b/docs/snippets/paused-execution-free.mdx @@ -1,4 +1,6 @@ In the Trigger.dev Cloud we automatically pause execution of tasks when they are waiting for longer than a few seconds. -When triggering and waiting for subtasks, the parent is checkpointed and while waiting does not count towards compute usage. When waiting for a time period (`wait.for` or `wait.until`), if the wait is longer than 5 seconds we checkpoint and it does not count towards compute usage. \ No newline at end of file +When triggering and waiting for subtasks, the parent is checkpointed and while waiting does not count towards compute usage. When waiting for a time period (`wait.for` or `wait.until`), a wait longer than 5 seconds does not count towards compute usage. + +Free compute isn't the same as freed concurrency: the concurrency slot is only released once we've snapshotted the machine and shut it down. For `wait.for` and `wait.until` that happens 60 seconds into the wait, so anything shorter stays `EXECUTING` and holds its slot for the whole wait. \ No newline at end of file