Rate limits & plans
The Archive API meters /api/v2 on two independent layers: a request-frequency wall and a
weighted cost model. The wall is the same on every plan; the weighted bucket is what your plan
sizes. Compare the plans below, then price your own query with the calculator.
Layer 1 — the request-frequency wall
Each workspace may send up to 5 requests per second to
POST /api/v2, on every plan (the default; it is environment-tunable). This wall counts requests,
not cost — a cheap query and an expensive one each consume one unit. Exceed it and the request is
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 wall 429 also carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
headers.
Layer 2 — the weighted cost model
The second layer prices each request by how much work it asks for and draws that price from a token bucket: the bucket has a capacity, refills at a steady rate, and each request withdraws its cost. Every workspace drains its own bucket — including a workspace that belongs to an organization, which is priced by the org’s plan but never shares one balance with its siblings. Two rules shape the price:
- A per-request floor. Every request costs a minimum of 5, covering the fixed overhead of authenticating and loading workspace context. A refund never takes your net cost below this floor.
- A post-execution refund. A connection’s cost is estimated up front from the page size you request, then reconciled after execution: if a page returns fewer records than you asked for, the unused per-record cost is refunded. Your actual cost is never more than 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 you nothing — it does not drain the bucket further. Both layers answer with
RATE_LIMIT_EXCEEDED; the message tells you which one you hit, and Retry-After is authoritative
either way.
Rate limits by plan
Each workspace draws from a token bucket: the bucket holds up to its capacity, refills at a steady rate, and every request withdraws its weighted cost.
| 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; every
mutation costs a flat 50.
Every mutation costs a flat 50, whatever its selection set.
38 weighted cost per request
- search_base
- 30
- ceil(0.3 × 20)
- +6
- edges { node } wrappers
- +2
* capped by the request-frequency wall, 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 the standard IETF RateLimit-Policy
and RateLimit headers. These are your source of truth for your current quota — read them rather
than hard-coding any 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.
Backoff guidance
- On a 429, honor the
Retry-Afterheader — wait the stated number of seconds before retrying. - Watch the
RateLimitheader’sr(remaining) andt(reset) to pace your weighted-cost spend and avoid draining the bucket. - Prefer smaller pages and only the fields you need; both reduce cost.