# Rate limits & quotas

> What each plan allows, how throttling behaves, and how to stay under it.

Source: https://hyperjobs.io/docs/rate-limits

Two independent limits apply to every key: a **per-minute rate** and a **monthly
request quota**. Both come from your plan.

## Plan limits

| Plan | Price | Requests / month | Requests / minute |
| --- | --- | --- | --- |
| Free | $0 | 500 | 10 |
| Starter | $49/mo | 50,000 | 60 |
| Growth | $199/mo | 500,000 | 300 |
| Scale | $499/mo | 5,000,000 | 1,000 |

Every plan gets the **full dataset and every filter**; plans differ only in
volume. Annual billing is 10× the monthly price — two months free — but isn't
self-serve yet; [email us](mailto:hello@hyperjobs.io) to set it up. Above Scale,
[talk to us](mailto:hello@hyperjobs.io) too — custom limits and flat-file
delivery are available.

Plan limits sync from the portal to the API about every five minutes, so the
limits you bought are the limits that are enforced — an upgrade is live within
minutes, no redeploy or new key needed.

<Note>
  Limits are per **account**, not per key. Ten keys on a Growth plan share one
  500,000-request monthly pool. Keys exist to separate environments and to be
  revoked independently, not to multiply quota.
</Note>

Your current usage is on the [dashboard](/dashboard), which shows
month-to-date consumption against your quota and a 30-day daily breakdown per
key.

## What counts as a request

Every authenticated HTTP request that reaches the API, including ones that
return zero results.

[`/v1/health`](/docs/api-reference/health) is free — it's unauthenticated, isn't
rate-limited, and doesn't touch your quota. Use it for liveness probes rather
than a `limit=1` job query. ([`/openapi.json`](https://api.hyperjobs.io/openapi.json)
is likewise keyless and free.)

## Rate-limit headers

Every authenticated response carries three headers:

| Header | Value |
| --- | --- |
| `X-RateLimit-Limit` | Your per-minute limit. |
| `X-RateLimit-Remaining` | Requests left in the current window. |
| `X-RateLimit-Reset` | Seconds until the window frees up. |

They describe the **per-minute** limit only — monthly quota consumption lives
on the [dashboard](/dashboard). `401` responses carry none of these; the
headers appear once a request has authenticated.

## Exceeding the per-minute rate

```json
{ "error": "rate limit exceeded", "rate_per_min": 300, "retry_after_s": 12 }
```

The window is a sliding 60 seconds, not a fixed calendar minute: the limit
counts requests in the last 60 seconds from *now*. So a burst of 300 at
12:00:00 on a Growth plan clears at 12:01:00, not at the top of the next minute.

`retry_after_s` — also sent as a standard `Retry-After` header — is how long
until the window frees. Wait that long and retry; if you're consistently
hitting the limit, pace the client rather than retry-looping.

## Exceeding the monthly quota

```json
{
  "error": "monthly quota exceeded",
  "monthly_quota": 50000,
  "resets": "first of next month (UTC)"
}
```

Also a `429`, but with no `Retry-After` header — retrying won't help. Requests
are rejected until the quota resets at the start of the next calendar month
(UTC), or until you upgrade. Upgrading takes effect within minutes — the new
quota applies to the current month, and there's no proration penalty for
mid-month upgrades. Both `429` shapes are in [errors](/docs/errors#429-rate-limited).

## Staying under the limit

The cheapest optimisation by far is **asking for fewer, bigger pages**. `limit`
goes to 200 on `/v1/jobs` and 1,000 on `/v1/jobs/feed`, and both cost exactly
one request:

```
50 pages × limit=20  = 50 requests
5 pages  × limit=200 = 5 requests   ← same data, 10× cheaper
```

The second cheapest is **not fetching descriptions you won't read**.
`description_format` defaults to omitting them; leave it off until you need
them. It doesn't affect your request count, but it's the difference between a
20KB and a 2MB response.

For a client that needs to sustain throughput, pace from the headers rather
than retrying on `429` — every response tells you exactly how much headroom is
left:

```js
// Read the budget off each response. When the window is nearly spent, wait out
// X-RateLimit-Reset. Pacing at the source costs nothing; discovering the limit
// via 429s costs a round-trip each time.
async function paced(url, key) {
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });

  const remaining = Number(res.headers.get("X-RateLimit-Remaining"));
  const reset = Number(res.headers.get("X-RateLimit-Reset"));
  if (remaining <= 1) {
    await new Promise((r) => setTimeout(r, reset * 1_000));
  }
  return res.json();
}

for (const url of urls) {
  await paced(url, key);
}
```

The window slides, so headroom refills continuously — no need to hardcode your
plan's rate; `X-RateLimit-Limit` tells you. If you run multiple workers, they
share one account-wide limit, so either give each worker a safety margin or
have them share the header state.

## Syncing efficiently

Re-crawling `/v1/jobs` to find what changed is the most expensive possible way
to stay current, and on a Free or Starter plan it will exhaust the monthly quota
before it finishes. Use [`/v1/jobs/feed`](/docs/api-reference/jobs-feed) — one
request returns everything posted since your last cutoff. See
[syncing the feed](/docs/guides/feed-sync).
