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

# Rate limits

> Per-minute and per-day limits, the headers that report them, and how to back off correctly.

Four limits apply, independently:

| Limit                 | Default    | Scope          | Applies to                                 |
| --------------------- | ---------- | -------------- | ------------------------------------------ |
| Requests per minute   | **60**     | Per key        | Everything                                 |
| Requests per day      | **50,000** | Per key        | Everything                                 |
| Requests per minute   | **300**    | Per IP address | Everything                                 |
| **Writes** per minute | **10**     | Per key        | `POST /rentals`, `POST /rentals/{id}/stop` |

<Note>
  The write limit is an **extra** ceiling, not a separate allowance: a write is
  counted against the general per-minute budget as well. Ten creates in a minute
  costs ten of your sixty requests, not ten of a private pool.

  It exists because writes spend money. A loop that has lost track of itself can
  issue sixty catalog reads a minute harmlessly; sixty rental creations a minute is
  a different kind of afternoon.
</Note>

Per-key overrides exist — `GET /me` reports yours in `rate_limit_per_min`, where
`null` means the account default. Test keys get a lower limit than live ones.
If your workload genuinely needs more, ask [help@gpuoutlet.ai](mailto:help@gpuoutlet.ai).

The per-IP limit is a separate rail from the per-key one, so a stolen key cannot
be used to hammer the API from a single host — and so one noisy tenant behind a
shared NAT does not get to spend everyone's budget.

## Headers on every response

```http theme={null}
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 43
```

`RateLimit-Reset` is **seconds until the window resets**, not a timestamp.

On a 429 you also get:

```http theme={null}
Retry-After: 12
```

## Treat `RateLimit-Remaining` as advisory

<Warning>
  Limits are enforced per API server instance and are approximate. Do not build a
  client that spends exactly `RateLimit-Remaining` requests and expects the next
  one to fail cleanly.
</Warning>

The honest model is: use the headers to *pace* yourself, and use 429 to *stop*.
A client that only reads headers will occasionally overshoot; a client that only
handles 429 will work but waste round trips. Doing both is a few lines.

## Backing off

```python theme={null}
resp = session.get(url, params=params)

if resp.status_code == 429:
    wait = int(resp.headers.get("Retry-After", 5))
    time.sleep(wait + random.random())   # jitter matters, see below
    resp = session.get(url, params=params)
```

Three rules:

1. **Honour `Retry-After`.** It is the server telling you exactly how long the
   window has left. Guessing produces either a wasted retry or a needless wait.
2. **Add jitter.** Twenty workers that all sleep exactly 12 seconds will all
   retry in the same millisecond and half will get 429 again. A random fraction
   of a second breaks the convoy.
3. **Cap the total.** Retrying forever turns a transient limit into a hung job.

## Daily quota

Separate from the per-minute limit, and it fails differently:

```json theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "daily_quota_exceeded",
    "message": "The daily request quota for this key is spent. It resets at 00:00 UTC.",
    "doc_url": "https://docs.gpuoutlet.ai/api/errors#daily_quota_exceeded",
    "request_id": "req_e08c2f5a71d4"
  }
}
```

Backing off for thirty seconds will not help here — the window is the rest of the
UTC day. Handle this code distinctly from `rate_limited`: alert, and stop the
loop, rather than sleeping in it.

## Staying well under

<AccordionGroup>
  <Accordion title="Page with limit=200, not limit=10" icon="list">
    The same 1,000 offers cost 5 requests instead of 100. The maximum page size is
    200; there is no reason to leave it at the default when you are walking a whole
    result set.
  </Accordion>

  <Accordion title="Cache reference data" icon="database">
    `/gpu-models`, `/regions` and `/prices` change on the order of days. Fetch them
    once at startup, not once per user action.
  </Accordion>

  <Accordion title="Poll provisioning rentals every 2–3 seconds, not every 200ms" icon="clock">
    Provisioning takes tens of seconds. Polling five times a second spends 300
    requests a minute — your entire budget — to learn the same thing a dozen requests
    would have told you.

    Once a rental is `running`, stop polling it altogether unless you have a reason.
  </Accordion>

  <Accordion title="Filter server-side" icon="filter">
    `/offers?gpu_family=h100-80gb&region=us-east-1&available=true` costs one request.
    Fetching the whole catalog and filtering in your process costs dozens and gives
    you the same answer, slightly staler. Every filter is listed in
    [Browsing the catalog](/api/offers).
  </Accordion>

  <Accordion title="One client, not one per thread" icon="share-nodes">
    The per-key limit is shared across every process using that key. Ten workers with
    their own idea of "60 per minute" produce 600.
  </Accordion>
</AccordionGroup>
