---
title: Rate Limits
description: >-
  Rate limits per authentication method, response headers, and 429 handling
  strategies
tags:
  - reference
  - rate-limits
  - throttling
---
The API counts requests per client and per route, and answers a 429 with how long to wait. Read this page when you're sizing a batch of calls, seeing throttling errors, or deciding how often to poll: it lists the per-minute and per-second ceilings, the headers a throttled response carries, and how to back off.

  beginner
  5 min
  [authentication](/docs/api/authentication)

## Rate Limits

Two layers throttle every web API request. The per-minute layer counts by client IP, so a Clerk JWT and an API key from the same machine share that budget; the burst layer counts per route and caller. Neither depends on which credential you send.

---

## Per-minute limits (every `/api` route)

The edge middleware picks a tier from the path and method, then counts requests per client IP over a one-minute window:

| Tier | Applies to | Requests per minute |
|------|-----------|:---:|
| Read | `GET` requests | 200 |
| Write | `POST`, `PUT`, `PATCH`, `DELETE` requests | 50 |
| Expensive | Paths containing `/compute`, `/rdkit`, `/orchestrator` or `/export` | 20 |
| Search | Paths containing `/search`, `/suggest` or `/autocomplete` | 120 |

## Burst limits (per route)

Each route also sets a one-second and a three-second ceiling. Job submission is the tightest:

| Route | 1 second | 3 seconds |
|-------|:---:|:---:|
| `POST /api/v1/jobs` | 2 | 5 |
| `GET /api/v1/jobs` | 5 | 12 |
| Harness session routes (create, get, checkpoints, events) | 6 | 15 |
| `POST /api/harness/sessions/:id/execute` and `/sync` | 10 | 30 |

### Guest calls

A caller with no key uses the keyless orchestrate request: one POST to `/api/v1/orchestrate` with a `prompt`. Its daily limit is stated once, with the request, in [Guest curl](/docs/api#guest-curl) and in [llms.txt](https://www.ivybiosciences.com/llms.txt). Each guest response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; read them rather than counting locally.

---

## Rate Limit Headers

Throttled responses (HTTP 429) carry `Retry-After`. The per-minute layer also sends its counters on the 429:

| Header | Description |
|--------|-------------|
| `Retry-After` | Seconds to wait before retrying |
| `X-RateLimit-Limit` | Maximum requests in the current window |
| `X-RateLimit-Remaining` | Remaining requests in the current window |
| `X-RateLimit-Reset` | When the window resets |
| `X-RateLimit-Tier` | Which per-minute tier refused the request |

Successful authenticated responses do not carry these headers; only the guest orchestrate route reports its remaining quota on every response.

---

## 429 Response Format

The per-minute layer answers:

```json
{
  "error": "Rate limit exceeded for Write-heavy API endpoints (POST/PUT/DELETE) endpoints",
  "retryAfter": 42
}
```

A burst limit answers with the standard error envelope:

```json
{
  "success": false,
  "code": "RATE_LIMITED",
  "message": "Too many requests. Please try again shortly.",
  "details": { "retryAfterSec": 1 }
}
```

---

## Handling Rate Limits

### TypeScript SDK

The SDK automatically retries 429 responses with exponential backoff (up to `config.retries` attempts):

```ts
const ivy = createIvyClient({
  apiKey: "ivy_your_key",
  retries: 3, // Retries 429 and 5xx up to 3 times
})
```

### Manual Retry (Any Language)

**TypeScript**

```ts
async function submitWithRetry(params: SubmitJobParams, maxRetries = 3) {
  for (let attempt = 0; attempt &lt; maxRetries; attempt++) {
    try {
      return await ivy.jobs.submit(params)
    } catch (e) {
      if (e instanceof IvyApiError && e.isRateLimited && attempt &lt; maxRetries - 1) {
        const wait = Math.pow(2, attempt) * 1000
        await new Promise(r => setTimeout(r, wait))
      } else {
        throw e
      }
    }
  }
}
```

**Python**

```python
import time
import requests

def submit_with_retry(params, max_retries=3):
    for attempt in range(max_retries):
        response = requests.post(
            f"{BASE_URL}/api/v1/jobs",
            json=params,
            headers=headers,
        )
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            print(f"Rate limited. Waiting {retry_after}s (attempt {attempt + 1})")
            time.sleep(retry_after)
            continue
        response.raise_for_status()
        return response.json()
    raise Exception("Max retries exceeded")
```

**cURL**

```bash
# On a 429, read how long to wait
curl -sI https://www.ivybiosciences.com/api/v1/jobs \
  -H "Authorization: Bearer ivy_your_key" | grep -i -E 'retry-after|x-ratelimit'

# Response headers on a throttled request:
# Retry-After: 42
# X-RateLimit-Remaining: 0
```

### React with Rate Limit UI

```tsx
import { IvyApiError } from "@ivybiosciences/sdk"
import { useIvyJob } from "@ivybiosciences/sdk/react"
import { useState } from "react"

function RateLimitAwareSubmit() {
  const [cooldown, setCooldown] = useState(0)

  const { submit, isLoading } = useIvyJob({
    onError: (e) => {
      if (e instanceof IvyApiError && e.isRateLimited) {
        setCooldown(60) // Show countdown
        const timer = setInterval(() => {
          setCooldown((c) => {
            if (c <= 1) { clearInterval(timer); return 0 }
            return c - 1
          })
        }, 1000)
      }
    },
  })

  return (
    <button disabled={isLoading || cooldown > 0}>
      {cooldown > 0 ? `Wait ${cooldown}s` : isLoading ? "Running..." : "Submit"}
    </button>
  )
}
```

---

## Best Practices

1. **Use the SDK** — built-in retry handles 429 automatically
2. **Honor `Retry-After`** — wait the stated seconds before the next attempt
3. **Batch when possible** — use the [Batch API](/docs/api/rest-batch) for multiple items
4. **Use WebSocket/SSE** — avoid polling for job status; subscribe to real-time updates instead
5. **Cache lookups** — public endpoints like `/lookup/uniprot/:id` return stable data suitable for caching

## Read next

- [Authentication](/docs/api/authentication) — which credential to send
- [Guest curl](/docs/api#guest-curl) — the keyless first call and its limit
- [SDK Errors](/docs/api/sdk-errors) — handling `IvyApiError.isRateLimited`
- [Server-sent events](/docs/api/realtime-sse) — follow a run without polling
