# Usage and limits

How your monthly quota is counted, how to check what is left, and what the rate limit and concurrency headers on every answer mean.

Ask the API how much of your allowance is left. This call is free.

```bash
curl "https://curlshot.com/api/v1/usage" \
  -H "X-Access-Key: YOUR_ACCESS_KEY"
```

```json
{
  "plan": { "slug": "free", "name": "Free" },
  "period": { "start": "2026-10-02T23:09:55.000Z", "end": "2026-11-01T23:09:55.000Z" },
  "quota": 100,
  "used": 12,
  "remaining": 88,
  "bonus_remaining": 25,
  "rate_limit_per_minute": 10,
  "concurrency": 1,
  "full_page_max_height": 10000,
  "full_page_max_scale": 1,
  "video_max_seconds": 10
}
```

The numbers above are a sample. Yours depend on your plan.

| Field | Meaning |
| --- | --- |
| `plan` | The plan your account is on: a short `slug` for code and a `name` for people. |
| `period` | The `start` and `end` of the current billing period. |
| `quota` | How many screenshots your plan gives you in this period. |
| `used` | How many of those you have taken so far. |
| `remaining` | How many of your plan's screenshots are left: `quota` minus `used`. |
| `bonus_remaining` | Extra screenshots left on top of your plan, for example from referrals. |
| `rate_limit_per_minute` | How many render requests your account may send per minute. |
| `concurrency` | How many renders may run at the same moment. |
| `full_page_max_height` | The tallest [full-page screenshot](https://curlshot.com/docs/full-page.md#limits-of-your-plan) your plan makes, in page pixels. |
| `full_page_max_scale` | The highest pixel density of a full-page screenshot on your plan. |
| `video_max_seconds` | The longest [video](https://curlshot.com/docs/video.md#limits-of-your-plan) your plan records, in seconds. `0` means the plan has no video capture. |

What you can still take in total is `remaining` plus `bonus_remaining`. In the sample that is 88 + 25 = 113.

## The three limits

Every plan has three limits. They protect different things, so it helps to keep them apart.

| Limit | What it counts | When it resets | Error when you pass it |
| --- | --- | --- | --- |
| Quota | Successful renders in the billing period | At the end of the period | [`quota_exceeded`](https://curlshot.com/docs/errors.md#quota_exceeded), status `402` |
| Rate limit | Render requests per minute, for the whole account | Within a minute | [`rate_limited`](https://curlshot.com/docs/errors.md#rate_limited), status `429` |
| Concurrency | Renders running at the same moment | As soon as a render ends | [`concurrency_limit`](https://curlshot.com/docs/errors.md#concurrency_limit), status `429` |

We do not print the numbers here, because they differ per plan. You find them on the [pricing page](https://curlshot.com/pricing), and your own values in the `GET /usage` answer above.

A plan also limits how large one full-page screenshot can be. That is not a fourth counter: a request above the size limit is not refused, the screenshot is made at the limit. A full-page screenshot always counts as one. See [Limits of your plan](https://curlshot.com/docs/full-page.md#limits-of-your-plan).

> **The limits belong to your account, not to a key**
>
> All API keys of one account share the same quota, the same rate limit and the same concurrency. Creating more keys does not give you more requests per minute.

## What counts against your quota

The rule is short: one successful render uses one screenshot.

| Request | Counted? |
| --- | --- |
| A screenshot or PDF that was rendered and returned | Yes, 1 |
| A [cache](https://curlshot.com/docs/caching.md) hit (`X-Cache: HIT`) | No |
| A request that failed with any error | No |
| Each successful job from an [async](https://curlshot.com/docs/async-and-webhooks.md) or [bulk](https://curlshot.com/docs/bulk.md) call | Yes, 1 per job |
| A call to `GET /usage`, or a status call for a job or a batch | No |
| A download of a stored file from its `url` | No |

The size of the screenshot does not matter. A full-page capture of a long article counts the same as a small thumbnail.

On top of your plan's quota you can have bonus screenshots, from things like referrals. The plan's own screenshots are used first. When they are gone, renders keep working until the bonus is used up too.

> **Tip**
>
> Turn on [`cache=true`](https://curlshot.com/docs/caching.md) for screenshots that are requested again and again. Repeat requests then cost nothing.

## Read your limits from any response

You do not have to call `/usage` to keep track. Every answer to a request with a valid key carries these headers:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com" \
  --output example.png -D -
```

The `-D -` part tells curl to print the response headers. Here are the ones about limits, for the sample account from the top of this page:

```http
HTTP/1.1 200 OK
content-type: image/png
cache-control: private, no-store
x-cache: MISS
x-concurrency-limit: 1
x-concurrency-remaining: 1
x-image-height: 1024
x-image-width: 1280
x-quota-limit: 100
x-quota-remaining: 112
x-quota-reset: 2026-11-01T23:09:55.000Z
x-ratelimit-limit: 10
x-ratelimit-remaining: 9
x-ratelimit-reset: 1791069120
x-reference-id: bcf05dcd0aae40cc84d621758c44e4f8
x-render-ms: 1240
x-request-id: bcf05dcd0aae40cc84d621758c44e4f8
```

Header names are not case-sensitive. curl prints them in lower case. We write them as `X-Quota-Limit` in the text because that is easier to read.

| Header | Meaning |
| --- | --- |
| `X-Reference-Id` | The id of this request. Quote it when you [contact support](https://curlshot.com/contact). `X-Request-Id` holds the same value. |
| `X-Quota-Limit` | Your plan's quota for the current period. |
| `X-Quota-Remaining` | Screenshots you can still take: what is left of your plan, plus your bonus screenshots. |
| `X-Quota-Reset` | When the period resets, as an ISO date such as `2026-11-01T23:09:55.000Z`. |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the rate limit resets, as Unix seconds (seconds since 1 January 1970). |
| `X-Concurrency-Limit` | Renders allowed at the same moment. |
| `X-Concurrency-Remaining` | Free render slots right now. |
| `X-Cache` | `HIT` for a stored copy, `MISS` for a new render. |
| `X-Render-Ms` | How long the render took, in milliseconds. |
| `X-Image-Width`, `X-Image-Height` | The size of the screenshot in pixels. Only on image answers. |
| `Retry-After` | Only on status `429` and `503`. How many seconds to wait before you try again. |

`X-Quota-Remaining` includes your bonus screenshots, so it can be larger than `X-Quota-Limit`. In the sample, 87 are left of the plan and 25 of the bonus, which makes `112`.

Note the two date styles. `X-Quota-Reset` is a readable date. `X-RateLimit-Reset` is a plain number of seconds.

## Status calls have their own rate limit

Some calls render nothing. They only report on something:

- `GET https://curlshot.com/api/v1/usage`
- `GET https://curlshot.com/api/v1/jobs/:id`, the status of an [async](https://curlshot.com/docs/async-and-webhooks.md) job
- `GET https://curlshot.com/api/v1/batches/:id`, the status of a [bulk](https://curlshot.com/docs/bulk.md) batch
- `GET https://curlshot.com/api/v1/files/:id` with an access key, a download of a stored file

These are free, and they are counted in a separate bucket. So checking on your jobs never uses up the requests you need for renders.

The bucket allows 10 times your plan's per-minute limit, and never less than 300 per minute. On a plan with 10 requests per minute, that is 300 status calls per minute. On a plan with 100, it is 1000.

On the answers to these calls, the three `X-RateLimit-*` headers describe this bucket, not your render limit:

```http
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-ratelimit-limit: 300
x-ratelimit-remaining: 299
x-ratelimit-reset: 1791069120
```

Your render limit is always in the body of `GET /usage`, as `rate_limit_per_minute`.

A stored file opened through its own `url`, the link with a `token` in it, is not counted at all.

## When the quota runs out

When `remaining` and `bonus_remaining` are both zero, the next render is refused:

```json
{
  "error_code": "quota_exceeded",
  "error_message": "The screenshot quota for this billing period is used up.",
  "documentation_url": "https://curlshot.com/docs/errors#quota_exceeded"
}
```

Retrying does not help here. The quota comes back when the period resets, at the time in `X-Quota-Reset`. To continue sooner, move to a larger plan on the [pricing page](https://curlshot.com/pricing).

## When you send requests too fast

Send more requests in a minute than your plan allows, and you get this:

```http
HTTP/1.1 429 Too Many Requests
content-type: application/json; charset=utf-8
retry-after: 12
x-ratelimit-limit: 10
x-ratelimit-remaining: 0
x-ratelimit-reset: 1791069120
```

```json
{
  "error_code": "rate_limited",
  "error_message": "Too many requests. Slow down and retry after the indicated delay.",
  "documentation_url": "https://curlshot.com/docs/errors#rate_limited"
}
```

You get the same status `429` with the code `concurrency_limit` when too many of your renders are running at once.

Neither one is a real failure. Nothing was rendered and nothing was counted. Wait, then send the request again.

## Fair use limits

The renderers are shared by all customers. Three more rules keep one account from slowing down the others. Most accounts never notice them.

| Rule | What it counts | The number | Error when you pass it |
| --- | --- | --- | --- |
| Bulk weight | A [bulk](https://curlshot.com/docs/bulk.md) call counts as one request per item against your rate limit. | Your rate limit | [`rate_limited`](https://curlshot.com/docs/errors.md#rate_limited), status `429` |
| Queue | [Async](https://curlshot.com/docs/async-and-webhooks.md) and bulk jobs of your account that are waiting or rendering at the same time. | 100 for each render of your concurrency, and never less than 200 | [`queue_limit`](https://curlshot.com/docs/errors.md#queue_limit), status `429` |
| Failed renders | Renders of your account that failed within the last minute. | Half your rate limit per minute, and never less than 5 | [`failure_limit`](https://curlshot.com/docs/errors.md#failure_limit), status `429` |

So with `rate_limit_per_minute` 60 and `concurrency` 5 from `GET /usage`, your queue holds 500 jobs and 30 renders may fail per minute. A plan can set its own numbers.

### A bulk call counts per item

A batch of 25 uses 25 requests of the current minute. When fewer are left, the call is refused and nothing is queued. A batch larger than your whole per-minute limit is accepted only when you have sent nothing else for a minute or two, and it uses the whole minute.

### The queue has a size

Jobs you start with `async=true` or with a bulk call wait in a queue. When the queue of your account is full, new jobs are refused:

```json
{
  "error_code": "queue_limit",
  "error_message": "This account already has 500 jobs waiting to render; your plan allows 500 at a time. Nothing was queued. Wait for some to finish, then retry.",
  "documentation_url": "https://curlshot.com/docs/errors#queue_limit"
}
```

A job leaves the queue when it is done or has failed. Sync requests, the ones that return the screenshot directly, are not jobs and do not count.

Two more things hold for jobs:

- Your jobs render at most `concurrency` at once, across all our renderers. This is separate from the slots of your sync requests.
- The queue serves accounts in turn: everyone's first waiting job, then everyone's second, and so on. A large batch from another customer does not delay your single job.

### Failed renders are free, up to a point

A failed render costs you nothing, but it used a browser on our side. So the number of renders that may fail per minute is capped. What counts is a render that started and did not finish: a `timeout`, a page that could not be loaded, a selector that was not found, a request you cancelled half-way. A request that is refused before anything renders, such as `invalid_options`, does not count. Neither does a problem on our side.

When the cap is reached, new renders are refused for the rest of that minute:

```json
{
  "error_code": "failure_limit",
  "error_message": "Too many renders of this account failed in the last minute (the limit on your plan is 30). Check the failing requests, then retry after the indicated delay.",
  "documentation_url": "https://curlshot.com/docs/errors#failure_limit"
}
```

The `Retry-After` header says how long to wait. [Cache](https://curlshot.com/docs/caching.md) hits and status calls are still answered, because they use no browser. Jobs that are already queued are not thrown away: they wait until the minute has passed, then continue.

If you see this error, something in your requests fails again and again. Fix that first. Waiting alone brings the same error back.

## Back off with Retry-After

Backing off means waiting before you retry, so you do not hit the limit again right away. The `Retry-After` header tells you how many seconds to wait.

```javascript
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function fetchWithBackoff(url, tries = 5) {
  for (let attempt = 1; attempt <= tries; attempt++) {
    const response = await fetch(url)
    if (response.status !== 429) return response

    // Wait as long as the API asks. Fall back to a growing pause if the header is missing.
    const seconds = Number(response.headers.get('retry-after')) || attempt * 2
    await sleep(seconds * 1000)
  }
  throw new Error('Still rate limited after several tries')
}
```

A fuller version, which also retries temporary render errors, is on the [errors page](https://curlshot.com/docs/errors.md#retry-with-backoff).

> **Common mistakes**
>
> - **You retry a `402` in a loop.** `quota_exceeded` does not go away by waiting a few seconds. Stop and check `/usage`.
> - **You retry a `429` at once.** That uses up the next minute as well. Wait for `Retry-After`.
> - **You send a big batch as separate requests, all at the same moment.** Use [bulk](https://curlshot.com/docs/bulk.md) or [async](https://curlshot.com/docs/async-and-webhooks.md). The jobs are queued for you.
> - **You read `X-RateLimit-Reset` as a date text.** It is a number of seconds. `X-Quota-Reset` is the one in ISO form.
> - **You count failed requests yourself.** Failed renders are free. Trust `used` and `X-Quota-Remaining`.
> - **You retry a failing request in a tight loop.** Free does not mean unlimited: after too many failed renders in a minute you get [`failure_limit`](#fair-use-limits). Fix the request, then retry.
> - **You expect a bulk call to count as one request.** It counts as one per item. See [Fair use limits](#fair-use-limits).
> - **You create a second API key to get a second rate limit.** The limits are per account. Every key draws from the same bucket.
> - **You read `X-RateLimit-Limit` on a `/usage` answer as your render limit.** On status calls it describes the status bucket. Read `rate_limit_per_minute` in the body.

## Where to go next

- [Errors](https://curlshot.com/docs/errors.md): every error code, and which ones are safe to retry.
- [Caching](https://curlshot.com/docs/caching.md): the simplest way to use fewer screenshots.
- [Bulk screenshots](https://curlshot.com/docs/bulk.md): start many renders with one request.
- [Pricing](https://curlshot.com/pricing): the quota, rate limit and concurrency of each plan.
