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

# Rate limits

> Burst and generation quota limits, the RateLimit-* headers, and how to handle 429 with backoff.

The Just Flow It API enforces two independent limits, both scoped **per API key**. Every response tells you exactly where you stand via the `RateLimit-*` headers, and a `429` response includes `Retry-After` so you know how long to wait before retrying.

<CardGroup cols={2}>
  <Card title="Burst limit" icon="gauge-high">
    **120 requests / 60s** across all endpoints. Smooths out spikes of traffic.
  </Card>

  <Card title="Generation quota" icon="wand-magic-sparkles">
    **500 generations / rolling 30 days**. Both `POST /diagrams/generate` and `POST /diagrams/{id}/edit` count.
  </Card>
</CardGroup>

## The two limits

<ResponseField name="Burst limit" type="120 requests / 60s">
  Applies to **every** request made with a given key, regardless of endpoint. When you exceed it you receive a `429` with code `rate_limit_exceeded`. The window is a rolling 60 seconds.
</ResponseField>

<ResponseField name="Generation quota" type="500 generations / rolling 30 days">
  Applies to `POST /diagrams/generate` and `POST /diagrams/{id}/edit` (both use scope `generate` and run an AI generation). Each accepted job counts once against the quota. When the quota is exhausted you receive a `429` with code `quota_exceeded`. The window is a rolling 30 days.
</ResponseField>

<Note>
  Both limits are tracked **per key**. A personal Pro key and a Team org key each have their own independent counters.
</Note>

## Response headers

Every API response carries the rate-limit headers, so you can read your remaining allowance without waiting for a `429`.

<ResponseField name="RateLimit-Limit" type="integer">
  The maximum number of requests allowed in the current window.
</ResponseField>

<ResponseField name="RateLimit-Remaining" type="integer">
  The number of requests remaining in the current window.
</ResponseField>

<ResponseField name="RateLimit-Reset" type="integer">
  The number of **seconds** until the current window resets.
</ResponseField>

<ResponseField name="Retry-After" type="integer">
  Present **only on `429` responses**. The number of **seconds** to wait before retrying.
</ResponseField>

### Example: a normal response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_8a1f2c3d4e5f
RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 42
```

<Tip>
  Read `RateLimit-Remaining` on every response and proactively slow down as it approaches `0`, rather than waiting to be throttled.
</Tip>

## Handling 429 responses

A `429 Too Many Requests` always has `error.type` of `rate_limit_error`. Inspect `error.code` to tell the two limits apart, then use the `Retry-After` header to decide how long to wait.

<CodeGroup>
  ```json rate_limit_exceeded (burst) theme={null}
  {
    "error": {
      "type": "rate_limit_error",
      "code": "rate_limit_exceeded",
      "message": "Too many requests. Slow down and retry after the indicated delay."
    },
    "request_id": "req_8a1f2c3d4e5f"
  }
  ```

  ```json quota_exceeded (monthly generations) theme={null}
  {
    "error": {
      "type": "rate_limit_error",
      "code": "quota_exceeded",
      "message": "Monthly generation quota exhausted. Quota resets on a rolling 30-day window."
    },
    "request_id": "req_9b2e3d4c5f60"
  }
  ```
</CodeGroup>

### Example: a 429 burst response

```http theme={null}
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-Request-Id: req_8a1f2c3d4e5f
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 17
Retry-After: 17
```

<Warning>
  `rate_limit_exceeded` is transient — wait `Retry-After` seconds and retry. `quota_exceeded` is **not** retryable within the window: retrying immediately will keep failing until your rolling 30-day generation count drops below 500. Back off until the quota resets, or upgrade your plan.
</Warning>

### Recommended strategy

<Steps>
  <Step title="Read the code">
    On a `429`, branch on `error.code`. Treat `rate_limit_exceeded` as retryable and `quota_exceeded` as a hard stop for the current window.
  </Step>

  <Step title="Honor Retry-After">
    Wait at least `Retry-After` seconds (it is provided in seconds) before retrying. Never retry sooner.
  </Step>

  <Step title="Add exponential backoff with jitter">
    For repeated `rate_limit_exceeded` responses, increase the delay on each attempt (e.g. `Retry-After`, then 2x, 4x…) and add random jitter to avoid thundering-herd retries. Cap the number of attempts.
  </Step>

  <Step title="Stop on quota_exceeded">
    Do not retry `quota_exceeded` in a loop. Surface it to the operator and resume after the rolling window frees up capacity.
  </Step>
</Steps>

### Backoff example

<CodeGroup>
  ```bash curl theme={null}
  # curl honors Retry-After only if you script it; this loop reads the
  # header and sleeps before retrying on a 429.
  url="https://justflow.it/api/v1/diagrams"
  for attempt in 1 2 3 4 5; do
    resp=$(curl -sS -D /tmp/h -o /tmp/b -w "%{http_code}" \
      -H "Authorization: Bearer jfi_sk_live_..." "$url")
    if [ "$resp" != "429" ]; then cat /tmp/b; break; fi
    retry=$(grep -i '^Retry-After:' /tmp/h | tr -d '\r' | awk '{print $2}')
    echo "429 received; sleeping ${retry:-2}s (attempt $attempt)"
    sleep "${retry:-2}"
  done
  ```

  ```javascript JavaScript (fetch) theme={null}
  async function requestWithBackoff(url, init, maxAttempts = 5) {
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      const res = await fetch(url, init);

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

      const body = await res.json();
      // Hard stop: quota will not free up by retrying now.
      if (body?.error?.code === "quota_exceeded") {
        throw new Error(`quota_exceeded (request_id=${body.request_id})`);
      }

      // Transient: honor Retry-After, then exponential backoff with jitter.
      const retryAfter = Number(res.headers.get("Retry-After")) || 1;
      const backoff = retryAfter * Math.pow(2, attempt) * 1000;
      const jitter = Math.random() * 250;
      await new Promise((r) => setTimeout(r, backoff + jitter));
    }
    throw new Error("rate_limit_exceeded: max attempts reached");
  }

  const res = await requestWithBackoff("https://justflow.it/api/v1/diagrams", {
    headers: { Authorization: "Bearer jfi_sk_live_..." },
  });
  ```

  ```python Python (requests) theme={null}
  import time, random, requests

  def request_with_backoff(url, headers, max_attempts=5):
      for attempt in range(max_attempts):
          res = requests.get(url, headers=headers)

          if res.status_code != 429:
              return res

          body = res.json()
          # Hard stop: retrying won't free quota in this window.
          if body.get("error", {}).get("code") == "quota_exceeded":
              raise RuntimeError(f"quota_exceeded (request_id={body['request_id']})")

          # Transient: honor Retry-After, then exponential backoff + jitter.
          retry_after = int(res.headers.get("Retry-After", "1"))
          backoff = retry_after * (2 ** attempt)
          time.sleep(backoff + random.uniform(0, 0.25))

      raise RuntimeError("rate_limit_exceeded: max attempts reached")

  res = request_with_backoff(
      "https://justflow.it/api/v1/diagrams",
      headers={"Authorization": "Bearer jfi_sk_live_..."},
  )
  ```
</CodeGroup>

## FAQ

<AccordionGroup>
  <Accordion title="Do RateLimit-* headers appear on every response?">
    Yes. `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` are returned on every API call. `Retry-After` is added only on `429` responses.
  </Accordion>

  <Accordion title="Which calls count against the generation quota?">
    Only `POST /diagrams/generate`. Each accepted job (returned as `202 Accepted` with a queued `Generation`) counts once. Polling `GET /generations/{id}` does not count against the quota, but every request still counts against the 120/60s burst limit.
  </Accordion>

  <Accordion title="Are the limits per key or per account?">
    Per key. Each key — personal Pro or Team org — maintains its own burst and quota counters.
  </Accordion>

  <Accordion title="What's the difference between rate_limit_exceeded and quota_exceeded?">
    Both are `429` with `error.type` `rate_limit_error`. `rate_limit_exceeded` means you hit the short-term burst limit (120/60s) — retry after `Retry-After` seconds. `quota_exceeded` means you exhausted the 500-generations rolling 30-day quota — retrying immediately won't help; wait for the window to free up or upgrade.
  </Accordion>
</AccordionGroup>
