# Handle Rate Limits and Outages Gracefully

> Source: https://agilitycms.com/docs/developers/handle-rate-limits-and-outages

A well-built Agility site keeps serving visitors when the content API is slow, rate limited or briefly unavailable. This page explains the limits you design around and the layers that make a site resilient: static generation, your own cache, Agility's CDN, a local copy with Content Sync, and retries with back-off.

For every status code and its meaning, see [Fetch API Status Codes and Caching](/docs/developers/fetch-api-status-codes-and-caching).

## What you are designing around

| Limit or event | What happens | Source |
| --- | --- | --- |
| More than 10 uncached requests in one second | `429 Too Many Requests` | [Content Fetch API](/docs/developers/content-fetch-api) |
| A request that takes longer than 30 seconds | `408 Request Timeout`, cached for 30 seconds | [Fetch API Status Codes and Caching](/docs/developers/fetch-api-status-codes-and-caching) |
| A response already on Agility's CDN | Served from the edge, and not counted towards the limit | [Content Delivery & CDN Architecture](/docs/developers/content-delivery-and-cdn-architecture) |
| A platform incident | Posted at [status.agilitycms.com](https://status.agilitycms.com), where you can subscribe to notifications | [SLA & Uptime](/docs/owners-admins/sla-and-uptime) |

The limit applies to requests that reach the API itself. Most traffic problems come from many **different** uncached URLs at once: a full static build, a cache that was just cleared, or code that fetches on every page view.

## The layers of resilience

Each layer reduces how often the next one is needed. Use as many as your site allows.

| Layer | What it protects against | How |
| --- | --- | --- |
| 1. Prerendered pages | Visitors never wait on the API | Static generation or incremental regeneration |
| 2. Your data cache | Repeated reads of the same content | Cache every published read, tagged, for a long time |
| 3. Webhook invalidation | Stale content without short cache lifetimes | Clear only the tags that changed on publish |
| 4. Agility's CDN | Load on the API from repeated identical requests | Automatic. Identical URLs are served from the edge |
| 5. A local copy | API dependency at read time | Content Sync into your own store |
| 6. Retries with back-off | Short `429`, `408` and network failures | Retry the right errors, with growing, randomized delays |
| 7. Serve stale on error | An API error replacing good content with nothing | Keep the last good copy when a refresh fails |

### 1. Prerender pages

When pages are rendered at build time or regenerated in the background, a visitor's request doesn't call Agility at all. [SLA & Uptime](/docs/owners-admins/sla-and-uptime) explains why this, together with edge caching, keeps published content available even during an origin event. On Next.js, see [Rendering & Data Fetching with Next.js](/docs/nextjs/next-js-and-server-side-rendering).

### 2 and 3. Cache every read, clear it on publish

Wrap every CMS read in your framework's cache with a tag per item, list, page and sitemap, and give it a long lifetime. When content is published, an Agility webhook calls your endpoint, which clears just those tags. This turns most page renders into cache hits and keeps content fresh within seconds of publishing. [Caching with Next.js and Agility](/docs/nextjs/caching-with-next-js-and-agility) has the full pattern.

Avoid short time-based lifetimes (for example 60 seconds) on a busy site: every expiry is another burst of uncached requests.

### 4. Make Agility's CDN work for you

Identical requests are cheap, because the CDN answers them. Different requests are not. So:

- **Ask for more per request.** Use `take` up to 250 on lists instead of many small pages, and `contentLinkDepth` (default 1, maximum 5) rather than one request per linked item.
- **Keep URLs stable.** The same query parameters in the same order make the same URL, which the CDN can serve again.
- **Use GraphQL** when a page needs fields from several lists. One request replaces several.

### 5. Keep a local copy with Content Sync

If your app must keep working without calling Agility at read time (offline kiosks, very high read volume, or a hard dependency you want to remove), sync content into your own store and read from there. Sync calls are the only API traffic, and a webhook or schedule keeps the copy current. See [Content Sync Explained](/docs/developers/content-sync-explained).

### 6. Retry the right errors with back-off

| Response | Retry? | How |
| --- | --- | --- |
| `429 Too Many Requests` | Yes | Wait, then retry with an exponential delay and random jitter |
| `408 Request Timeout` | Yes, after at least 30 seconds | The 408 is cached for 30 seconds, so a quicker retry of the same URL gets it again. Ask for less data |
| `5xx`, or a network error | Yes, a few times | Exponential delay with jitter |
| `400 Bad Request` | No | Fix the request, usually the `filter` |
| `401 Unauthorized` | No | Fix the key or its type |
| `404 Not Found` | No | Treat as "not there" |

Cap the number of attempts, and give up quickly while a visitor is waiting. Retrying hard during a build is fine; retrying hard inside a page request makes the visitor wait.

> [!IMPORTANT]
> `@agility/content-fetch` (2.0.11, checked 2026-10-03) does not retry, and it does not throw on an error response or a network failure. It logs the error and resolves to `undefined`. If you use it, check every result. An `undefined` that reaches your cache or your page is cached or rendered as empty content.

This wrapper calls the REST API directly, retries what is safe to retry, and throws when it gives up:

```ts
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504])

export async function fetchAgility(url: string, apiKey: string, maxAttempts = 4) {
  for (let attempt = 1; ; attempt++) {
    let status = 0
    try {
      const res = await fetch(url, { headers: { APIKey: apiKey } })
      if (res.ok) return res.json()
      status = res.status
      if (!RETRYABLE.has(status)) {
        throw new Error(`Agility returned ${status} for ${url}`) // 400, 401, 404: don't retry
      }
    } catch (err) {
      if (status && !RETRYABLE.has(status)) throw err
      if (attempt >= maxAttempts) throw err
    }
    if (attempt >= maxAttempts) throw new Error(`Agility returned ${status} for ${url}`)

    const base = status === 408 ? 30_000 : 500 * 2 ** (attempt - 1) // 0.5 s, 1 s, 2 s...
    const delay = base + Math.random() * base // jitter spreads retries out
    await new Promise((r) => setTimeout(r, delay))
  }
}
```

Whether 429 responses carry a `Retry-After` header is not documented. If you see one, wait at least that long.

### 7. Serve stale content when a refresh fails

The goal of everything above: an error should never replace good content with nothing.

- **Throw, don't return empty.** When a read fails, throw. Frameworks with background regeneration keep serving the last good version when regeneration throws. Next.js documents this for [incremental static regeneration](https://nextjs.org/docs/app/guides/incremental-static-regeneration): "If an error is thrown while attempting to revalidate data, the last successfully generated data will continue to be served from the cache."
- **Use stale-while-revalidate where you control the cache.** Serve the cached copy straight away and refresh in the background, so a slow API slows the refresh, not the visitor. Your host's CDN may also support `stale-while-revalidate` and `stale-if-error` in `Cache-Control`; check its documentation.
- **Have a fallback for optional parts of a page.** A failed "related articles" list can be left out. A failed page body should fail the render so the last good page stays in place.

## Builds without bursts

A static build of a large site is the most common source of `429` responses, because it renders many pages at once and each page makes several requests.

- **Limit build concurrency.** See [Troubleshooting: "fetch failed" Errors in Next.js](/docs/nextjs/troubleshooting-fetch-failed-errors-in-next-js).
- **Fetch shared data once.** Read the sitemap, navigation and other shared lists once per build and reuse them, rather than once per page.
- **Don't prerender everything.** Prerender your most visited pages and let the rest render on first request.
- **Stagger builds** when several sites read from one instance.

## Webhook storms

Publishing many items at once sends many webhooks. Acknowledge each one quickly, queue the work, and clear caches in batches. Agility collapses repeated identical events within a 30-second window, but don't rely on it. See [Webhook Events and Payload Reference](/docs/developers/webhook-events-and-payloads).

## Monitoring

- Log every `429`, `408` and `5xx` from Agility with the URL, so you can see which code path causes them.
- Count cache hits and misses on your side. A sudden drop in hits explains most rate-limit incidents.
- Subscribe to [status.agilitycms.com](https://status.agilitycms.com) for platform incidents.

## A checklist

- [ ] Pages are prerendered or regenerated in the background
- [ ] Every published read is cached with a tag and cleared by webhook
- [ ] No time-based lifetimes shorter than you need
- [ ] Preview reads bypass the cache; published reads never do
- [ ] SDK results are checked; failures throw instead of returning empty
- [ ] `429`, `408`, `5xx` and network errors retry with back-off and jitter; `400`, `401` and `404` don't
- [ ] Build concurrency is limited
- [ ] Optional page sections have a fallback
- [ ] Someone is subscribed to the status page

## Related

- [Fetch API Status Codes and Caching](/docs/developers/fetch-api-status-codes-and-caching)
- [Content Delivery & CDN Architecture](/docs/developers/content-delivery-and-cdn-architecture)
- [Content Sync Explained](/docs/developers/content-sync-explained)
- [Caching with Next.js and Agility](/docs/nextjs/caching-with-next-js-and-agility)
- [SLA & Uptime](/docs/owners-admins/sla-and-uptime)
- [Reference Architectures](/docs/developers/reference-architectures)
