Limits and slots
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.
Limits
Section titled “Limits”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');import runstate
# 60 units per minute, shared by every worker in the space.@runstate.limit("search-api", max=60, per="minute")def search_web(query: str) -> list: ...
# Waits for a unit, then calls search_web.results = search_web("acme funding rounds")maxandperdeclare the limit.peris 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_configpermission. Later calls check that the sizes match and raiseConflictErrorif they don’t. - Leave out
maxandperto use a limit that already exists, for example one created in the console or withsetup(). Worker keys needcoordination_readandcoordination_writeto use a resource declared elsewhere. Only creating a resource needsresource_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 raisesTimeoutError.
Take units without a function
Section titled “Take units without a function”When the call you want to limit isn’t a single function, take units directly:
await runstate.take('search-api'); // one unitawait runstate.take('search-api', { units: 5, timeout: '2m' }); // five units, wait up to two minutesrunstate.take("search-api") # one unitrunstate.take("search-api", units=5, timeout="2m") # five units, wait up to two minutes# in async code: await runstate.atake("search-api")Things to know about limits
Section titled “Things to know about limits”- 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
maxa 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');# Five browser sessions for the whole space.@runstate.slots("browser", max=5)async def open_page(url: str) -> str: ...
# Waits for a free slot, runs open_page, gives the slot back.html = await open_page("https://example.com/pricing")In Python, slots also works as a context manager when the work isn’t one function:
with runstate.slots("browser"): page = browser.new_page() page.goto("https://example.com/pricing")
# in async codeasync with runstate.slots("browser"): ...maxdeclares 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 raisesTimeoutError.- If the process loses its slot, for example because it was paused for longer than the lease,
context().signalaborts and the call raisesLeaseLostError.
Combine them
Section titled “Combine them”Wrappers stack. A function can hold a slot and take limit units:
const browse = runstate.slots('browser', runstate.limit('search-api', openPage), { max: 5 });@runstate.slots("browser", max=5)@runstate.limit("search-api")def open_page(url: str) -> str: ...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.
Plan limits
Section titled “Plan limits”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.
Related
Section titled “Related”- Spend budgets: the same “reserve before you start” idea, for money.
- TypeScript SDK and Python SDK reference.
- Other languages: HTTP API reference.