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 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.
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
Use a concurrency limit
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 samedelay to every trigger doesn’t collapse anything. Each trigger still creates its own run. It just starts later:
Avoid
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
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
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
Callingruns.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
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
ListingQUEUED 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()
{ 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(orfrom/to) that covers what you need, likeperiod: "1h". - Filter by
taskIdentifieras 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 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.
