---
title: "Rate limits"
description: "Concurrency and throughput limits on the OnePageCRM API, and how to back off when you hit them."
canonical_url: https://developer.onepagecrm.com/api/rate-limits/
source: Markdown mirror of https://developer.onepagecrm.com/api/rate-limits/
---

The API enforces limits on concurrency and request rate. There is no
per-day quota. Two different layers enforce them, and they signal
differently:

- **Connection limits** return HTTP `429 Too Many Requests`.
- **The request-rate throttle** returns HTTP `403` with a plain-text
  body — `Rate Limit Exceeded` — not `429`, and not the JSON envelope.

Handle both as "slow down".

## The limits

| Limit | Behavior | Signal when exceeded |
| --- | --- | --- |
| Concurrent connections | Capped per client — keep a small pool | HTTP `429` |
| Request rate | Sustained high rates are throttled; short bursts are tolerated | HTTP `403`, `text/plain` body `Rate Limit Exceeded` |

The exact thresholds are operational and can change without notice —
treat the *signals* as the contract, not any specific number. The
limits are generous for normal integrations: keep a few concurrent
connections, avoid unbounded parallelism, and you will rarely see
either signal. They exist to catch runaway loops.

Don't confuse a throttle `403` with a permission `403`. The throttle
sends a `text/plain` body with exactly `Rate Limit Exceeded`;
permission errors send the JSON envelope with an `error_name`. Check
the content type before treating a `403` as a rate limit — see
[Errors](/api/errors/).

## Handling throttles: back off with jitter

When you get a `429`, or a `403` with the plain-text
`Rate Limit Exceeded` body, wait and retry. Double the wait on each
consecutive failure, and add jitter so parallel clients don't retry in
lockstep.

```js
async function isThrottled(res) {
  if (res.status === 429) return true;
  if (res.status !== 403) return false;
  // The rate throttle returns 403 with a plain-text body.
  const type = res.headers.get("content-type") ?? "";
  if (!type.includes("text/plain")) return false;
  return (await res.clone().text()).includes("Rate Limit Exceeded");
}

async function fetchWithBackoff(url, options, maxRetries = 5) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, options);
    if (!(await isThrottled(res)) || attempt >= maxRetries) return res;

    const base = Math.min(1000 * 2 ** attempt, 30_000); // 1s, 2s, 4s... cap 30s
    const delay = base / 2 + Math.random() * (base / 2); // add jitter
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}
```

The same shape in Python:

```python
import random
import time

import requests

def is_throttled(response):
    if response.status_code == 429:
        return True
    if response.status_code != 403:
        return False
    # The rate throttle returns 403 with a plain-text body.
    content_type = response.headers.get("content-type", "")
    return "text/plain" in content_type and "Rate Limit Exceeded" in response.text

def get_with_backoff(url, max_retries=5, **kwargs):
    for attempt in range(max_retries + 1):
        response = requests.get(url, **kwargs)
        if not is_throttled(response) or attempt == max_retries:
            return response
        base = min(2 ** attempt, 30)  # 1s, 2s, 4s... cap 30s
        time.sleep(base / 2 + random.uniform(0, base / 2))
```

## Tips for staying under the limits

- **Limit concurrency, not just rate.** A pool of 2–3 connections is
  plenty for most integrations.
- **Use [pagination](/api/pagination/) with `per_page=100`** instead of
  many small requests.
- **Cache reference data.** Statuses, deal stages, and custom field
  schemas change rarely — fetch them once and cache them, not per
  request.
- **Prefer webhooks over polling** where you can — react to changes
  instead of asking for them.

## 503 during maintenance

During maintenance windows the API returns HTTP `503` with a
maintenance JSON body (`error_name: service_unavailable`,
`error_message: "System offline for maintenance"`). This is not a rate
limit. Pause and retry later — hammering a `503` just delays your own
recovery.

Treat throttle responses and `503` differently in code: a throttle
means *slow down*, `503` means *come back later*.

## Common questions

**What are the API rate limits?**
Two layers: a cap on concurrent connections (signals with HTTP `429`)
and a request-rate throttle (signals with HTTP `403` and a plain-text
`Rate Limit Exceeded` body). There is no per-day quota. Exact
thresholds are operational and can change — treat the signals as the
contract, not any specific number.

**Does the API return 429 when I'm throttled?**
Only for the concurrency cap. The request-rate throttle returns `403`
with a plain-text `Rate Limit Exceeded` body — not `429`, and not the
JSON envelope. Check the content type before treating a `403` as a
permission error.

**How should I handle a throttle response?**
Wait and retry with exponential backoff and jitter: double the wait on
each consecutive failure and cap it at around 30 seconds. A `503` is
maintenance, not a throttle — pause and come back later instead.

## What to read next

- **[Errors](/api/errors/)** — the full status code table and envelope.
- **[Pagination](/api/pagination/)** — fetch large datasets in fewer requests.
- **[API reference](/api/reference/)** — every endpoint, parameter, and response shape.
