Throttle
Space requests by rate and cap concurrency, pooled per stitch or per host.
Add throttle to a stitch when you need to stay under an API's rate limit and
keep in-flight requests bounded — it spaces calls to a target rate and caps how
many run at once, before any request leaves.
Example
import { } from 'stitchapi';
const = ({
: 'https://api.example.com',
: '/search',
: { : '5/s', : 2, : 'host' },
});Calls to search() are spaced to five per second with at most two running
concurrently. When a call has to wait, the stitch emits a progress event with
phase throttled, so a waiting call is visible on the event stream rather than
silently stalled.
Options
rate is a string like "5/s" — a minimum spacing between successive calls,
not a token bucket. concurrency caps simultaneous in-flight calls; either may
be set on its own.
The rate grammar
A rate is <count>/<duration>. The count is a whole number; the denominator is
any duration token, and a bare unit
means one of that unit — so '2/s' is '2/1s'.
| Rate | Means | Spacing |
|---|---|---|
'2/s' | 2 per second | 500ms |
'1000/h' | 1000 per hour | 3600ms |
'100/15m' | 100 per 15 minutes | 9000ms |
'2/500ms' | 2 per half-second | 250ms |
Write the limit the way your vendor publishes it. A quota of "1000 requests per
hour" is '1000/h' — you don't have to convert it to a per-minute count, and
you couldn't do so exactly anyway (it's 16.67/m).
A rate is a ratio, so equal ratios are the same limiter. '2/500ms',
'4/s' and '240/m' all declare a 250ms gap and behave identically — the
window length is a way of spelling the ratio, not a burst allowance. Because
this is a pacer rather than a token bucket, there is no capacity for a
longer window to grant: calls are spaced, never released in a burst. If you
need to spend a real quota the way the vendor accounts for it, hand the
backoff to an outer gate with delegate instead — and when the vendor
prices each call differently, as Shopify's point bucket does, rate limits
priced in query cost shows why no
single rate is the right one.
A rate that can't be honored is rejected when the stitch is built, rather than
silently ignored — a dropped rate is an unlimited one. That covers the obvious
typos ('fast', '2/nope') and both degenerate ends: a zero count ('0/s'),
and a spacing longer than the ~24.8-day timer ceiling ('1/30d').
When the rate is all you need, pass it directly — a bare string is shorthand for
rate, exactly like timeout: '5s' is shorthand for timeout: { total: '5s' }:
import { } from 'stitchapi';
const = ({
: 'https://api.example.com/ping',
: '1/s', // ≡ { rate: '1/s' }
});The shorthand expands at compose time, so __config.throttle always reads back
as the object form, and a throttle: '1/s' layered over an inherited
{ rate: '5/s', concurrency: 2 } replaces only the rate.
pool decides what shares the budget: 'stitch' (the default) gives each
stitch its own budget, while 'host' pools the budget across every stitch
hitting the same host — use it when one API publishes a single account-wide
limit. Host pooling works in-process out of the box: separate stitches in
the same process draw from one budget per host with no extra configuration.
Giving those stitches a shared
store extends that single-process budget
into a distributed one that spans
workers.
Anti-pattern: don't rely on a host-pooled throttle to honor an
account-wide limit once you run more than one process — the budget lives in
memory and is process-local, so each of N workers keeps its own count and
together they hit api.example.com at up to N times the rate you declared;
pass a shared store so every worker draws from one budget instead. See
Distributed throttle.
A throttled wait surfaces as a throttled progress phase on the event
stream — see The event stream.
See Reference → Config types for every field.