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

# Rate limits and bounded retries

> Handle 429, Retry-After, and retry layers with a read-only diagnostic example.

## Identify the limiting layer

Limits can originate from reverse proxies, gateway user or model limits, channel capacity, and upstream providers. Values and windows depend on deployment configuration; OpenRouter plan limits do not describe your own instance. Additional API keys do not necessarily increase a user's aggregate allowance.

| Result | Suggested handling |
| - | - |
| 429 | Reduce concurrency and bound queues; inspect available `Retry-After` and request logs |
| 503 | Capacity or a failed limit check may be responsible; investigate using the request ID |
| 401 / 403 | Check credentials and grants; do not retry in a loop |
| 400 / 404 | Check parameters, paths, and models; retries cannot add an absent endpoint |
| Disconnect / timeout | Execution is uncertain; do not assume generation or task submission did no work |

The Redis rejection path in current `middleware/model-rate-limit.go` sets `Retry-After` in seconds. Its in-memory implementation can reject without that header and with an empty body. Other limiting paths and upstream behavior differ, so do not require JSON error bodies or uniform rate-limit headers on every 429.

`Retry-After` can contain seconds or an HTTP date. It suggests when to try again, without guaranteeing success. Use jittered backoff when absent or invalid. If the wait exceeds your total deadline, stop instead of shortening the server's requested wait and retrying early.

## Runnable: discover models and record diagnostic metadata

Save `examples/diagnostics.mjs` with [http-client.mjs](/en/guides/http-runtime) in the same directory. Run from the documentation directory:

```sh theme={null}
export API_ROOT_URL='https://gateway.example.com'
export API_KEY='YOUR_API_KEY'
node examples/diagnostics.mjs
```

Keep deployment path prefixes in `API_ROOT_URL`, excluding `/v1`. The script only calls `GET /v1/models`, with at most 3 attempts and a 10-second total deadline. Only 429 / 502 / 503 / 504 are retried. Network errors, redirects, other HTTP errors, and invalid success responses stop immediately. Error bodies are not printed; success bodies are limited to 1 MiB and only the model count is output. stderr receives status, attempt, cumulative elapsed time, and a format-checked request ID when each response's headers arrive.

```javascript theme={null}
import { setTimeout as delay } from 'node:timers/promises';
import { readBody } from './http-client.mjs';

// http-client validates API_ROOT_URL and API_KEY on import. The root excludes /v1.
const url = `${process.env.API_ROOT_URL.replace(/\/+$/, '')}/v1/models`;
const started = performance.now();
const deadline = started + 10_000;
const signal = AbortSignal.timeout(10_000);
try {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.API_KEY}` },
      redirect: 'error', signal,
    });
    const id = response.headers.get('x-gateway-request-id')
      || response.headers.get('x-oneapi-request-id');
    console.error(JSON.stringify({
      event: 'models_response', attempt, status: response.status,
      elapsed_ms: Math.round(performance.now() - started),
      request_id: id && /^[A-Za-z0-9._:-]{1,128}$/.test(id) ? id : null,
    }));
    if (!response.ok) {
      await response.body?.cancel();
      if (attempt === 3 || ![429, 502, 503, 504].includes(response.status)) {
        throw new Error('Non-retryable status or attempt limit.');
      }
      const header = response.headers.get('retry-after')?.trim();
      let retryAfter = 0;
      if (header) {
        const parsed = /^\d+$/.test(header) ? Number(header) * 1000 : Date.parse(header) - Date.now();
        if (!Number.isNaN(parsed)) retryAfter = Math.max(0, parsed);
      }
      const backoff = Math.random() * 200 * 2 ** (attempt - 1);
      const wait = Math.max(retryAfter, backoff);
      // Never shorten a server-requested wait to fit the total deadline.
      if (wait >= deadline - performance.now()) throw new Error('Retry exceeds deadline.');
      await delay(wait, undefined, { signal });
      continue;
    }
    if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') || '')) {
      await response.body?.cancel();
      throw new Error('Expected JSON.');
    }
    const result = JSON.parse((await readBody(response, 1024 * 1024)).toString('utf8'));
    if (!result || result.error || result.success === false || result.object !== 'list'
      || !Array.isArray(result.data)
      || result.data.some((item) => !item || typeof item.id !== 'string' || !item.id.trim())) {
      throw new Error('Invalid model list.');
    }
    console.log(JSON.stringify({ models_count: result.data.length }));
    break;
  }
} catch {
  console.error('Model discovery failed. Check the status and request ID above. No model generation was attempted.');
  process.exitCode = 1;
}
```

This example demonstrates bounded recovery for a read-only operation. Do not apply it unchanged to generation, uploads, or task creation. Generation examples retain `maxRetries: 0`. Successful model discovery does not verify inference capability; see [production acceptance](/en/guides/production).

## Application controls

Bound concurrency, queue duration, and queue length; allocate budgets by actual user or business task. Include queueing, SDK retries, and internal gateway attempts in the total deadline. Preserve interrupted streaming state instead of silently appending regenerated content. Poll an existing task rather than resubmitting it.

[Model routing](/en/guides/model-routing) · [Request observability](/en/guides/observability) · [Error handling](/en/guides/errors)

[阅读中文](/zh/guides/rate-limits)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.