Rate limits & plans
The Archive API applies two independent limits to /api/v2. The request rate limit is the same
on every plan. The weighted cost limit is sized by your plan. Compare the plans below, then
price your own query with the calculator.
The request rate limit
Each workspace can send up to 5 requests per second to
POST /api/v2, on every plan. That is the default; Archive can tune it per environment. This limit counts
requests, not cost: a cheap query and an expensive one each count as one request. Requests over the
limit are rejected with HTTP 429 and a Retry-After header:
{
"errors": [
{
"message": "Rate limit exceeded for this workspace. Retry in 1 second(s).",
"extensions": { "code": "RATE_LIMIT_EXCEEDED" }
}
]
}A rate-limit 429 also carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
headers.
The weighted cost limit
The second limit prices each request by the work it asks for. The price is drawn from a token bucket. The bucket holds up to its capacity, refills at a steady rate, and each request withdraws its cost.
Each workspace has its own bucket. A workspace in an organization takes its capacity from the organization’s plan. It never shares a balance with the organization’s other workspaces.
Two rules affect the cost of a request:
- A per-request floor. Every request costs at least 5. The floor covers the fixed work of authenticating and loading workspace context. A refund never takes your net cost below it.
- A post-execution refund. A connection’s cost is estimated up front from the page size you request. After execution, the estimate is reconciled. If a page returns fewer records than you asked for, the unused per-record cost is refunded. Your actual cost never exceeds the estimate.
A request that asks for more than the bucket has left is rejected before any field is resolved,
with HTTP 429 and a Retry-After header:
{
"errors": [
{
"message": "Rate limit exceeded. Retry after 3 seconds.",
"extensions": { "code": "RATE_LIMIT_EXCEEDED" }
}
]
}A rejected request costs nothing and doesn’t drain the bucket further. Both limits return
RATE_LIMIT_EXCEEDED. The message names the limit you hit, and Retry-After is authoritative in
both cases.
Rate limits by plan
The table lists each plan’s bucket capacity and refill rate.
| Plan | Bucket capacity | Refill rate | Full refill | Bucket |
|---|---|---|---|---|
| Trial | 500 | 5 / s | 100 s | Own bucket — never pooled, even inside an organization |
| Startup | 5,000 | 50 / s | 100 s | Per workspace |
| Growth | 15,000 | 250 / s | 60 s | Per workspace |
| Enterprise | 60,000 | 1,000 / s | 60 s | Per workspace |
| Agency | 15,000 | 250 / s | 60 s | Each org workspace drains its own bucket at Growth numbers |
RateLimit-Policy header
rather than hard-coding a capacity.
Query cost calculator
A connection’s page cost is search_base + ceil(per_edge × first), plus what you select inside it
and any operation surcharge. Every request costs at least 5.
A mutation costs a base of 50, plus 1 for each field you select in its
payload, nested fields included. For example,
createCollection(input: …) { collection { id } userErrors { message } } selects four fields
(collection, id, userErrors, message) and costs 54. Select only
the payload fields you read.
A mutation costs a base of 50, plus 1 for each field you select in its payload. The total shows the base; add your payload fields to it.
38 weighted cost per request
- search_base
- 30
- ceil(0.3 × 20)
- +6
- edges { node } wrappers
- +2
* capped by the request rate limit, not by the bucket.
The calculator opens on a real request — this one:
query ItemsDefault($first: Int, $after: String) {
items(first: $first, after: $after) {
totalCount
edges {
node {
id
provider
type
caption
takenAt
originalUrl
archivePublicUrl
currentEngagement {
likes
comments
views
earnedMediaValue
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
} {
"first": 10
} Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a curl https://app.archive.com/api/v2 \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer docs_demo_token_0000000000000000000000' \
-H 'WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a' \
-d '{
"query": "query ItemsDefault($first: Int, $after: String) {\n items(first: $first, after: $after) {\n totalCount\n edges {\n node {\n id\n provider\n type\n caption\n takenAt\n originalUrl\n archivePublicUrl\n currentEngagement {\n likes\n comments\n views\n earnedMediaValue\n }\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}",
"variables": {
"first": 10
}
}' const response = await fetch("https://app.archive.com/api/v2", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer docs_demo_token_0000000000000000000000",
"WORKSPACE-ID": "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a"
},
body: JSON.stringify({
query: `query ItemsDefault($first: Int, $after: String) {
items(first: $first, after: $after) {
totalCount
edges {
node {
id
provider
type
caption
takenAt
originalUrl
archivePublicUrl
currentEngagement {
likes
comments
views
earnedMediaValue
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}`,
variables: {
"first": 10
}
})
})
console.log(JSON.stringify(await response.json(), null, 2)) require "json"
require "net/http"
query = <<~'GRAPHQL'
query ItemsDefault($first: Int, $after: String) {
items(first: $first, after: $after) {
totalCount
edges {
node {
id
provider
type
caption
takenAt
originalUrl
archivePublicUrl
currentEngagement {
likes
comments
views
earnedMediaValue
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
GRAPHQL
response = Net::HTTP.post(
URI("https://app.archive.com/api/v2"),
{ query: query, variables: { "first" => 10 } }.to_json,
"Content-Type" => "application/json",
"Authorization" => "Bearer docs_demo_token_0000000000000000000000",
"WORKSPACE-ID" => "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a"
)
puts response.body import json
import requests
query = """
query ItemsDefault($first: Int, $after: String) {
items(first: $first, after: $after) {
totalCount
edges {
node {
id
provider
type
caption
takenAt
originalUrl
archivePublicUrl
currentEngagement {
likes
comments
views
earnedMediaValue
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
"""
response = requests.post(
"https://app.archive.com/api/v2",
json={"query": query, "variables": {"first": 10}},
headers={
"Authorization": "Bearer docs_demo_token_0000000000000000000000",
"WORKSPACE-ID": "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a",
},
)
print(json.dumps(response.json(), indent=2)) Reading the RateLimit headers
Every /api/v2 response that carries rate-limit state includes RateLimit-Policy and RateLimit
headers, modeled on the
IETF RateLimit header fields draft.
They are the source of truth for your current quota. Read them; don’t hard-code a capacity from the
table above:
RateLimit-Policy: "<plan>";q=<capacity>;w=<window>
RateLimit: "<plan>";r=<remaining>;t=<reset-epoch> In RateLimit-Policy, q is the bucket capacity and w is the number of seconds to refill the
full bucket. In RateLimit, r is how much you have remaining and t is the epoch second at
which the bucket is full again.
Backing off
- On a 429, honor the
Retry-Afterheader: wait the stated number of seconds before retrying. - Watch the
RateLimitheader’sr(remaining) andt(reset timestamp) values to pace your weighted-cost spend and avoid draining the bucket. - Prefer smaller pages and request only the fields you need; both reduce cost.