Skip to main content
runs.list() (and GET /api/v1/runs) is built for browsing, reporting, and bulk operations. It is not a coordination tool. Each call searches your environment’s runs in the requested time window, so its cost depends on how many runs you created in that window much more than on the page size you ask for. A call with limit: 4 in a busy environment can still search millions of runs to find those 4. That’s fine once in a while. On a hot path, like before every trigger or in a loop every second, it slows your own code down and counts against your API rate limit.

Quick reference

Anti-patterns

Checking for active runs before triggering

This pattern tries to make sure only one run handles a piece of work at a time:
Avoid
It has three problems:
  • It races. Two calls can both see “nothing running” and both trigger.
  • It’s expensive. Every change runs a search, and the search gets slower as you create more runs.
  • It doesn’t collapse bursts. 100 changes in a second still mean 100 searches.
Let Trigger.dev do the coordination instead. To collapse bursts into one run, use debounce. While a debounced run is still waiting for its delay, triggers with the same key push it back instead of creating new runs:
Use debounce
With debounce, one run handles many changes. Have the task read the pending work from your own database when it starts, instead of relying on a single trigger’s payload.
To never run two at once for the same key, use a per-key concurrency limit. Extra runs wait in the queue instead of running in parallel:
Use a concurrency limit
The concurrency option requires SDK 4.7.0 or later. See Concurrency for details. Debounce only merges triggers into a run that hasn’t started yet. Once that run is queued or executing, the next trigger creates a new run. Combine both options so bursts collapse and a new run never overlaps one that’s still going. For exactly one run per piece of work, use an idempotencyKey. See Using a random idempotency key below.

Using a fixed delay to space out triggers

Adding the same delay to every trigger doesn’t collapse anything. Each trigger still creates its own run. It just starts later:
Avoid
Use debounce with the same key instead, so triggers within the delay join one run.

Using a random idempotency key

An idempotency key only dedupes triggers that send the same key. A fresh random value on every trigger never matches, so every call creates a new run:
Avoid
Build the key from the thing that should only happen once:
Your backend
Inside a task, a raw string key is scoped to the current run. To dedupe across runs, create it with idempotencyKeys.create(key, { scope: "global" }). See Idempotency for scopes and TTLs.

Polling for a run’s status

Avoid
Inside a task, use triggerAndWait(). You don’t pay for compute during waits longer than 5 seconds. From your backend, subscribe to the run. The loop ends when the run finishes:
Use realtime

Polling runs by tag to follow progress

Calling runs.list({ tag }) on a timer to see what a user’s or a job’s runs are doing repeats the same search every time. Subscribe once instead:
Use realtime
This iterator never completes on its own, so break out of the loop when you’re done. In a frontend, use the React hooks.
Tag subscriptions only include runs created in the last 24 hours. You can narrow that with the createdAt option, but not extend it. To follow a specific older run, use runs.subscribeToRun(runId).

Counting runs to measure a backlog

Listing QUEUED runs to count them gets slower as the backlog grows, which is exactly when you need the number. It can also miss runs: queued runs created before the time window aren’t returned. The queue already tracks the count:
Use queues.retrieve()
For a queue shared between tasks, use { type: "custom", name: "your-queue-name" }.

When you do need runs.list()

runs.list() is the right tool for dashboards, reports, audits, and finding runs to act on in bulk. To keep it fast:
  • Pass a time window. Use the narrowest period (or from/to) that covers what you need, like period: "1h".
  • Filter by taskIdentifier as well as status or tag.
  • Stop paginating once you have what you need. Don’t page through every result by default.
  • Keep it off hot paths. Don’t call it per event, per trigger, or in a tight loop inside a task. If several places need the same answer, cache it.
If you don’t pass period, from, or to, only runs created in the last 7 days are searched. The window applies to every status, so runs created before it aren’t returned even if they’re still queued or executing. Passing only to removes the lower bound, so the search covers every run up to that date.
If a query would be too expensive to complete, the API returns a 422 asking you to narrow the time window. The SDK doesn’t retry it.