Under heavy development
StitchAPI
GuidesResilience

Delegate backoff

Surface rate-limit outcomes as a RateLimitError so an outer gate owns the backoff, instead of retrying and throttling them internally.

By default a stitch handles a rate limit itself: a 429 listed in retry.on is retried with backoff, the Retry-After header is consumed to pace that wait, and the built-in throttle spaces calls before they leave. That's the right behavior when the stitch is the only thing talking to the API.

It's the wrong behavior when something outside StitchAPI already owns the rate budget — an outer gate or circuit with its own Retry-After hook and a persisted backoff window. There, internal retry hides the rate-limit signal from the gate, and the internal throttle double-counts against it. Turn on delegate-backoff mode and the stitch surfaces the rate-limit outcome instead of absorbing it, so the outer gate decides what to do.

Example

import { ,  } from 'stitchapi';

// Your own rate-gate / circuit, living outside StitchAPI.
declare const : { (: number): void };

const  = ({
    : 'https://api.example.com',
    : '/pages/{id}',
    : { : true }, // surface 429s; don't retry or throttle them
});

try {
    await ({ : { : '42' } });
} catch () {
    if ( instanceof ) {
        // Hand the signal to your outer gate — it owns the backoff.
        .(. ?? 1000);
    }
}

A 429 no longer retries: the call rejects with a RateLimitError carrying status, the retryAfterMs parsed from Retry-After (delta-seconds or an HTTP-date), and the raw response so you can read any other rate headers the API sent. The internal throttle is bypassed for the call, so it adds no pacing of its own.

Options

delegate switches the mode on; it defaults to false, so a stitch behaves exactly as before until you opt in.

on is the set of statuses treated as a rate-limit signal — it defaults to [429]. Widen it when an API signals back-pressure with another status (for example { delegate: true, on: [429, 503] }); a status not in on is left to the ordinary retry path.

Reading it off the stream

If you iterate the event stream instead of awaiting, the same outcome arrives as an error event carrying status and retryAfterMs, so a streaming consumer gets the identical structured hint:

import {  } from 'stitchapi';

// Your own rate-gate / circuit, living outside StitchAPI.
declare const : { (: number): void };

const  = ({
    : 'https://api.example.com',
    : '/pages/{id}',
    : { : true },
});

for await (const  of .({ : { : '42' } })) {
    if (. === 'error' && . !== ) {
        .(.);
    }
}

What still runs

Delegate mode changes only how a rate-limit status is handled. Everything else on a stitch is untouched: input validation, templating, transform, unwrap, and drift all still run on a successful response, and non-rate-limit failures (a 500, a transport error) behave exactly as they do without the flag. A circuit block, if you also set one, still applies — you can let the host layer both.

The throttle block becomes inert for a stitch in delegate mode — the host owns the gate, so the stitch never acquires its internal limiter and never emits a throttled event. If you still want StitchAPI to pace calls, don't delegate: use retry + throttle instead.

Delegate mode and internal retry-on-rate-limit are mutually exclusive for the delegated statuses: a status in rateLimit.on is surfaced, never retried, even if it also appears in retry.on.

See Reference → Config types for every field.

See also

On this page