Skip to content
HomeConsoleGet started

Give the whole swarm one shared rate limit or one set of slots, and let agents wait their turn instead of retrying together.

Your agents may run on ten machines, but your search API, browsers and GPUs have one set of limits. runstate gives the swarm two ways to share them:

Tool Use it for While it waits
limit Rate limits: N units per window, such as 60 search calls per minute. A fair queue on the server, served as each new window opens.
slots Things held while work runs: browsers, GPU jobs, database connections. A queue on the server, served as slots come back.

Both wrap a function. Every worker that calls the wrapped function draws from the same shared resource, and nobody retries in a loop.

import runstate from 'runstate-sdk';
// 60 units per minute, shared by every worker in the space.
const search = runstate.limit('search-api', searchWeb, { max: 60, per: 'minute' });
// Waits for a unit, then calls searchWeb.
const results = await search('acme funding rounds');
  • max and per declare the limit. per is a number of seconds, a duration such as '10s', or 'second', 'minute', 'hour' or 'day'.
  • The first call creates the limit if it doesn’t exist. That needs a key with the resource_config permission. Later calls check that the sizes match and raise ConflictError if they don’t.
  • Leave out max and per to use a limit that already exists, for example one created in the console or with setup(). Worker keys need coordination_read and coordination_write to use a resource declared elsewhere. Only creating a resource needs resource_config.
  • units (default 1) is how many units each call takes. Use it when one call costs more, such as a batch request.
  • timeout (default 60 seconds) is how long a call waits. After that it raises TimeoutError.

When the call you want to limit isn’t a single function, take units directly:

await runstate.take('search-api'); // one unit
await runstate.take('search-api', { units: 5, timeout: '2m' }); // five units, wait up to two minutes
  • Units are spent when taken. They are not given back, even if the function fails or the process crashes before using them.
  • The queue is fair. Callers are served in order. A caller asking for many units at the head of the queue holds back smaller requests behind it.
  • A waiting caller outlives its process for a short while. If a process dies while waiting, its place in the queue stays until it expires, and it can be served units that then go unused.
  • Your limit and the provider’s can drift. Set max a little below the provider’s real limit.
// Five browser sessions for the whole space.
const browse = runstate.slots('browser', openPage, { max: 5 });
// Waits for a free slot, runs openPage, gives the slot back.
const html = await browse('https://example.com/pricing');
  • max declares how many slots exist, created or checked the same way as a limit.
  • The slot is always given back when the function returns or throws.
  • While the function runs, the SDK keeps the slot alive by renewing its lease (lease, default 30 seconds). If the process dies, the slot comes back when the lease lapses.
  • units (default 1) is how many slots each call holds. timeout (default 60 seconds) is how long a call waits before it raises TimeoutError.
  • If the process loses its slot, for example because it was paused for longer than the lease, context().signal aborts and the call raises LeaseLostError.

Wrappers stack. A function can hold a slot and take limit units:

const browse = runstate.slots('browser', runstate.limit('search-api', openPage), { max: 5 });

Stacked wrappers take their resources one after the other, so a caller can hold a slot while it waits for a limit unit. When work needs several resources at once, put it on a queue that names them: a worker then takes the job and every resource in one step.

Your organization also has an “agents working at once” limit that counts held slots, once calls in progress and queue jobs being handled. When you reach it, the SDK waits and retries. See Limits & plans.