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.
curl "https://curlshot.com/api/v1/usage" \
-H "X-Access-Key: YOUR_ACCESS_KEY"{
"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 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 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, status 402 |
| Rate limit | Render requests per minute, for the whole account | Within a minute | rate_limited, status 429 |
| Concurrency | Renders running at the same moment | As soon as a render ends | concurrency_limit, status 429 |
We do not print the numbers here, because they differ per plan. You find them on the pricing page, 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.
#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 hit (X-Cache: HIT) | No |
| A request that failed with any error | No |
| Each successful job from an async or bulk 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.
#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:
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/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: bcf05dcd0aae40cc84d621758c44e4f8Header 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. 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/usageGET https://curlshot.com/api/v1/jobs/:id, the status of an async jobGET https://curlshot.com/api/v1/batches/:id, the status of a bulk batchGET https://curlshot.com/api/v1/files/:idwith 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/1.1 200 OK
content-type: application/json; charset=utf-8
x-ratelimit-limit: 300
x-ratelimit-remaining: 299
x-ratelimit-reset: 1791069120Your 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:
{
"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.
#When you send requests too fast
Send more requests in a minute than your plan allows, and you get this:
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{
"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 call counts as one request per item against your rate limit. | Your rate limit | rate_limited, status 429 |
| Queue | Async 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, 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, 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:
{
"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
concurrencyat 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:
{
"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 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.
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.
#Where to go next
- Errors: every error code, and which ones are safe to retry.
- Caching: the simplest way to use fewer screenshots.
- Bulk screenshots: start many renders with one request.
- Pricing: the quota, rate limit and concurrency of each plan.