> ## 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

# Rate Limits

Limits are applied **per API key and per endpoint**. Your key's traffic never consumes another
customer's allowance, and spending the budget on one endpoint doesn't affect the others.

Most endpoints enforce no limit today. The ones that do are listed below, and they 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.

<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`     | Requests allowed in the window                          |
| `X-RateLimit-Remaining` | Requests still available                                |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the allowance is restored |
| `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: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 1757000000
```

## 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"
  }
  ```
</CodeGroup>

Both 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).
