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

# Rate Limits

> Which Dcycle API endpoints are throttled, the headers to read, and how to handle a 429

Limits are applied **per API key**. Your key's traffic never consumes another customer's
allowance.

Two kinds of limit apply, and both return the rate limit headers on **every** response — not
just on a 429 — so a client can pace itself before it is ever turned away:

* an **account-wide limit** on the total requests your key makes across all endpoints, and
* **per-endpoint limits** on a few heavy endpoints, listed below, which are counted separately
  and on top of the account-wide one.

<Info>
  Limits can change as the API evolves. Read the response headers rather than hardcoding the
  numbers on this page.
</Info>

## Response headers

| Header | Description |
| - | - |
| `X-RateLimit-Limit` | The most requests you can make right now: the bucket's capacity (the burst) |
| `X-RateLimit-Remaining` | Requests still available. Never more than `X-RateLimit-Limit` |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the bucket is full again |
| `RateLimit-Policy` | The policy, IETF format: `"<name>";q=<requests>;w=<seconds>;burst=<capacity>` |
| `RateLimit` | The same state, IETF format: `"<name>";r=<remaining>;t=<seconds until full>` |
| `Retry-After` | Seconds to wait before retrying. Sent only on a `429` |

```http Example response headers theme={"theme":{"light":"github-light","dark":"github-dark"}}
HTTP/1.1 200 OK
X-RateLimit-Limit: 900
X-RateLimit-Remaining: 899
X-RateLimit-Reset: 1791196724
RateLimit-Policy: "baseline_api_key";q=600;w=60;burst=900
RateLimit: "baseline_api_key";r=899;t=1
```

## Account-wide limit

Every request your API key makes counts against one budget, whatever the endpoint:

| | Limit |
| - | - |
| Sustained | **600 requests / minute** |
| Burst | up to **900** requests may arrive together before the sustained rate applies |

The budget refills continuously (a token bucket), not in fixed minute windows: after a burst you
regain one request every 100 ms. A rejected request does not consume budget, so backing off for
the `Retry-After` interval always recovers on schedule.

Logistics endpoints (`/v1/logistics/*`, `/v2/logistics/*`) are excluded from the account-wide
limit and governed only by their per-endpoint limits below.

## Limited endpoints

### Request rate

How many requests you may send per unit of time.

| Endpoint | Limit |
| - | - |
| `POST /v1/logistics/requests` | 30 requests / second |
| `POST /v1/logistics/recharges` | 30 requests / second |
| `POST /v2/logistics/requests` | 30 requests / second |

### Concurrent requests

Bulk endpoints limit how many of your requests may be **in flight at the same time**, rather
than how many you send per second. A bulk call does a lot of work per request, and the cap keeps
one client from occupying every worker.

| Endpoint | Limit |
| - | - |
| `POST /v1/logistics/requests/bulk` | 10 concurrent requests |
| `POST /v1/logistics/recharges/bulk` | 10 concurrent requests |
| `POST /v2/logistics/requests/bulk` | 10 concurrent requests |

<Note>
  Concurrency limits are about parallelism, not pacing. Send bulk calls from a pool of at most 10
  workers and you will never see a `429` from them, however long each call takes.
  These responses carry `Retry-After` but no `X-RateLimit-*` headers — there is no window to
  report on.
</Note>

## When you hit a limit

The API answers `429 Too Many Requests`.

<CodeGroup>
  ```json Rate limit theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "detail": "Rate limit exceeded"
  }
  ```

  ```json Concurrency limit theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "detail": "Too many concurrent requests"
  }
  ```

  ```json Upstream provider limit theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "code": "UPSTREAM_RATE_LIMITED",
    "detail": "Upstream service rate limit exceeded, retry later"
  }
  ```
</CodeGroup>

The third body means a provider we depend on (for example the identity service behind
`/auth/login`) throttled the call; it carries `Retry-After: 1` and clears within seconds.

On `/v2/wastes`, `/v2/ingest-jobs` and `/v2/webhook-endpoints` the same `429` comes as
[problem details](/api-reference/errors), like every other error of those resources:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#TOO_MANY_REQUESTS",
  "title": "Too Many Requests",
  "status": 429,
  "code": "TOO_MANY_REQUESTS",
  "detail": "Rate limit exceeded",
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

All of them carry a `Retry-After` header. **Wait that long before retrying** — retrying sooner just
earns another `429`.

## Handling 429 correctly

Honour `Retry-After` when it is present, and back off exponentially with jitter when it isn't.
Jitter matters: without it, a fleet of clients that all got throttled retries in lockstep and
throttles itself again.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import random
  import time

  import requests

  MAX_ATTEMPTS = 5


  def post_with_retry(url: str, payload: dict, headers: dict) -> requests.Response:
      """POST, backing off when the API asks us to."""
      for attempt in range(MAX_ATTEMPTS):
          response = requests.post(url, json=payload, headers=headers, timeout=30)
          if response.status_code != 429:
              return response

          # Retry-After is authoritative; the fallback is exponential with jitter.
          wait = float(response.headers.get("Retry-After", 2**attempt))
          time.sleep(wait + random.uniform(0, 1))

      raise RuntimeError(f"still rate limited after {MAX_ATTEMPTS} attempts")
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const MAX_ATTEMPTS = 5;

  async function postWithRetry(url, payload, headers) {
    for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
      const response = await fetch(url, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });

      if (response.status !== 429) return response;

      // Retry-After is authoritative; the fallback is exponential with jitter.
      const retryAfter = Number(response.headers.get('Retry-After') ?? 2 ** attempt);
      await new Promise((r) => setTimeout(r, retryAfter * 1000 + Math.random() * 1000));
    }

    throw new Error(`still rate limited after ${MAX_ATTEMPTS} attempts`);
  }
  ```
</CodeGroup>

## Staying under the limits

<AccordionGroup>
  <Accordion title="Use the bulk endpoints">
    One bulk call carrying 500 shipments costs a single request against the limit; 500 individual
    calls cost 500. See [Create Requests Bulk](/api-reference/logistics/create-requests-bulk).
  </Accordion>

  <Accordion title="Read the headers as you go">
    `X-RateLimit-Remaining` tells you how much budget is left before you spend it. Slowing down at
    a low remaining count is cheaper than recovering from a `429`.
  </Accordion>

  <Accordion title="Cap your own concurrency">
    Size your worker pool to the concurrency limit of the endpoint you are calling — 10 for the
    bulk endpoints — instead of firing every request at once and retrying the rejections.
  </Accordion>

  <Accordion title="Spread scheduled jobs">
    Nightly syncs that all start exactly at 00:00 pile into the same window. Starting them at a
    random offset within a few minutes removes the spike without changing the total work.
  </Accordion>
</AccordionGroup>

## Need a higher limit?

Tell us the endpoint, the throughput you need and the shape of your traffic (steady, or a daily
batch) and we will size it with you. See [Support](/docs/support).


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