> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usefused.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle provider limits and failures

> Rate limits, retries, and pagination handled once in the runtime instead of in every feature.

An application makes one call. What happens when the provider rate-limits it, returns a 503, or splits the answer across forty pages is decided here — once, on the workspace service, and inherited by every SDK and MCP server that uses it.

The outcome: no feature anyone on your team writes needs its own retry loop, token bucket, or page walker.

<Note>
  This is workspace config, and only workspace config. `rate_limit`, `retry`, and `pagination` are rejected in `sdk.yaml` and `mcp.yaml` — generated clients contain no local window, semaphore, retry loop, jitter, or sleep, and that is deliberate.
</Note>

## Where it goes

```yaml theme={null}
apiVersion: fused/v1
kind: workspace
services:
  stripe:
    execution_policy:          # default for every version below
      rate_limit: { }
      retry: { }
      pagination: { }
    versions:
      - version: "2024-06-20"
        execution_policy:      # overrides the default, for this version only
          rate_limit: { }
```

A version-level policy wins over the service default for that version. Apply it the ordinary way:

```bash theme={null}
fused-cli workspace plan
fused-cli workspace apply
```

## Rate limits and concurrency

`rate_limit.policies` is a list of dimensions enforced at the same time — a per-minute cap and a concurrency ceiling can coexist.

```yaml theme={null}
rate_limit:
  version: 3
  policies:
    - name: minute_requests
      mode: enforce
      unit: requests
      identity: {inputs: [{kind: connection}]}
      algorithm: fixed_window
      fixed_window: {limit: 300, duration_ms: 60000}
```

| Property                      | What it does                                                                       | Values                                                    |
| ----------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `mode`                        | Enforce the limit, or measure it without blocking                                  | `enforce`, `observe`                                      |
| `unit`                        | What gets counted                                                                  | `requests`, `points`, `complexity`, `quota_units`         |
| `identity.inputs`             | What shares one bucket                                                             | `connection`, `account`, named shared credential family   |
| `cost.default` / `cost.rules` | What one call costs; rules override per operation                                  | any number                                                |
| `algorithm`                   | Exactly one branch, configured in a block of the same name                         | `fixed_window`, rolling window, token bucket, concurrency |
| `response_signals`            | Read the provider's own limit, remaining, reset or cost from a header or body path | —                                                         |
| `cooldown`                    | Back off on a status range or `Retry-After`, bounded by `max_delay_ms`             | —                                                         |

<Tip>
  Start with `mode: observe` to learn a provider's real ceiling before enforcing one. And because identity can be a shared credential family, a limit can be coordinated across every caller using the same provider account — something client-side limiting cannot do.
</Tip>

## Retries

`retry.rules` is evaluated in order. Values within one predicate field are ORed, fields are ANDed, and the first matching rule's action applies.

```yaml theme={null}
retry:
  version: 3
  rules:
    - predicates:
        statuses: [{min: 429, max: 429}, {min: 500, max: 599}]
        body_replayability: replayable
      action:
        max_attempts: 3
        backoff: {strategy: exponential, base_delay_ms: 250, max_delay_ms: 5000, jitter_ms: 100}
```

| Predicate                   | Matches on                                               |
| --------------------------- | -------------------------------------------------------- |
| `methods`                   | HTTP methods                                             |
| `operation_kinds`           | `read`, `write`, `delete`, `stream`, `query`, `mutation` |
| `statuses`                  | Status ranges, as `{min, max}`                           |
| `body_replayability`        | Whether the body can be sent again                       |
| `idempotency_key`           | `{requirement: any \| required \| absent, header}`       |
| `required_provider_headers` | Headers that must be present                             |

| Action                | What it bounds                                           |
| --------------------- | -------------------------------------------------------- |
| `max_attempts`        | Total tries                                              |
| `max_elapsed_ms`      | Wall clock across all tries                              |
| `backoff`             | `strategy`, `base_delay_ms`, `max_delay_ms`, `jitter_ms` |
| `retry_after_headers` | Which provider headers to honour, and their cap          |

<Note>
  Retrying a write needs explicit `body_replayability` and `idempotency_key` predicates. A `POST` is not automatically idempotent, and guessing wrong duplicates real records.
</Note>

`retry_config` is an accepted alternative spelling. Setting both is rejected.

## Pagination

The Engine walks the pages and streams each one as a separate chunk; your client makes a single call.

```yaml theme={null}
pagination:
  version: 3
  request:
    - {state: cursor, target: {location: query, name: cursor}, value_type: string, apply: subsequent}
  response:
    items: {path: "$.items"}
    values:
      - {name: next_cursor, source: {location: body, path: "$.metadata.next_cursor"}}
  continuation:
    - {kind: token, state: cursor, response_value: next_cursor}
  termination: {stop_on_empty_items: true, stop_on_missing_values: [next_cursor], repeated_value: error}
  limits: {max_pages: 100, max_items: 10000, max_bytes: 16777216, max_duration_ms: 120000}
```

| Property          | What it does                                                                                                                            |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `request`         | Binds state or constants to a query, header, body or GraphQL-variable target. `apply` scopes it to `all`, `first` or `subsequent` pages |
| `response.items`  | Where the page's items live                                                                                                             |
| `response.values` | Named values pulled from a body path, header, RFC Link, GraphQL result, or the last item                                                |
| `continuation`    | How the next page is addressed — `token`, `offset`, `page`, RFC Link, or next URL. These compose                                        |
| `termination`     | When to stop, and what a repeated value means                                                                                           |
| `limits`          | `max_pages`, `max_items`, `max_bytes`, `max_duration_ms` — all four required                                                            |

<Warning>
  A next-URL continuation needs an explicit same-origin or allowlist policy. Without one, a provider response could redirect the Engine at an unreviewed origin while carrying your credential.
</Warning>

Callers may only *lower* the ceiling, via `max_pages`. Strategy, tokens, paths and next URLs are never caller-owned.

## Fixing a wrong base URL

When a spec declares the wrong server, or none at all:

```yaml theme={null}
execution_policy:
  base_url: "https://api.example.com/current"
  server_variables: {tenant: acme, region: eu1}
```

`base_url` layers on top of the spec-derived value, which stays intact and inspectable underneath. `server_variables` binds imported OpenAPI server-template variables — keys must name variables declared on the effective operation server, and at most 128 entries are allowed.

<Warning>
  These values are **literal only**. They are validated against `^[A-Za-z0-9._~-]{1,512}$`, so a `${bucket.env.KEY}` reference is rejected here. Dynamic bucket values are an app-level feature: use an [SDK or MCP injection](/bucket/store-credentials#inject-a-bucket-value-into-a-request) with `location: server_variable`. Where both exist, this workspace map is the final authority.
</Warning>

A connection-resource forced binding (`${resource.base_url}`) still wins at dispatch time. `execution_policy.base_url` is the static fallback beneath it, not a way to override a live connection's routing.

`server_variables` never leaves your Engine, even with `public: true`, and `workspace sync` preserves the local map. There is no owner-editable path for `default_headers` today.

## Local effect vs publishing

These are two independent things, and confusing them is the most common mistake here.

|                    | Effect                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| Declaring a policy | Takes effect **locally and immediately on apply** — whether or not you own the service, whether or not `public` is set |
| `public: true`     | *Additionally* publishes it to the Registry so every other consumer of the service inherits it                         |

So a non-owner declaring `execution_policy` without `public` is not a no-op — it is the normal case: enforce this here, affect nobody else. Only the owning account can set `public: true`; the Engine rejects it from anyone else at apply.

Other Engine deployments that inherit a published value get a `registry_execution_policy_changed` notification the next time their poller runs.

<Note>
  Do not confuse this `public` with a service's own top-level `public`, which controls Registry visibility of the service page. Both are owner-only and both are called `public`, but they publish entirely different things.
</Note>

## Clearing an override

```yaml theme={null}
execution_policy:
  reset: true
```

`reset` drops your **local** override for that tier back to the Registry-sourced snapshot. It touches nothing you have published — there is no unpublish; a prior publish stands until superseded. Pair `reset` with nothing else.

<Card title="Know when a policy changes" icon="bell" href="/notifications">
  Inheriting a Registry default means someone else can change it. That is what the notification is for.
</Card>
