IMPORTANT: Rate limits
GraphQL Rate Limits
We have rate limits in place for the GraphQL API to ensure fair access and protect server stability.
There are four types of limits, each protecting against a different kind of overload:
| Limit | What it protects against | Status code |
|---|---|---|
| Request rate | Too many requests per time window | 429 |
| Query complexity | Single queries that request too much data | 200 (GraphQL error) |
| Server-time budget | Cumulative server processing time per client | Informational only; no budget-based rejection |
| Concurrency | Too many simultaneous queries across all clients (per-client and global) | 503 |
Request rate limits
Current limits
Maximum of 3600 requests per 15 minutes (averages at 4 requests per second).
Rate limits for the GraphQL API are per API key, regardless of the IP.
Rate limit headers
Responses processed by the request-rate limiter include RateLimit headers:
-
RateLimit-Limit— number of requests allowed in the window -
RateLimit-Policy— informative quota showing the limit with a time window in seconds -
RateLimit-Remaining— how many requests are remaining in the current window -
RateLimit-Reset— how many seconds until the rate limit resets
See the IETF draft for rate limit headers. At the moment, we use draft version 06, but we may upgrade to version 08 or newer.
How to handle rate limits
Avoiding rate limits
When you get a response from the API, check for the following.
-
Is the HTTP status 200? If not, and if it’s 429 (Too Many Requests), continue to the When you get rate limited section.
-
Does the
Ratelimit-Remainingheader have a sufficient value for your needs? If not, you may need to change your processes.
When you get rate limited
Do not attempt to request more data until the time in RateLimit-Reset header passes (e.g. time of your request + reset seconds). We recommend waiting an extra second afterwards so you don’t hit a rate limit right when the rate limit period is ending.
Add a timeout to your request until you can request more data.
If you keep hitting rate limits repeatedly, we will reach out to you.
Query complexity limits
Every GraphQL query has a complexity score based on how much data it requests. Queries that exceed the complexity limit are rejected before execution.
The main cost driver is nested connections with large page sizes. The first (or last) argument on connections determines how many items are fetched, and nested connections multiply.
How complexity is calculated
-
Each field costs 1 point
-
Connection fields (like
steamApps,followerHistory) cost proportional to theirfirst/lastargument -
Nested connections multiply.
steamApps(first: 100)withfollowerHistory(first: 100)inside costs roughly 100 x 100 = 10,000 effective rows
Examples
| Query pattern | Approx. rows | Complexity | Result |
|---|---|---|---|
steamApps(first: 50) — flat, several fields | 50 | ~350 | Allowed |
steamApps(first: 1000) — flat | 1,000 | ~7,000 | Allowed |
steamApps(first: 10) { followerHistory(first: 90) } | 900 | ~3,600 | Allowed |
steamApps(first: 100) { followerHistory(first: 365) } | 36,500 | ~146,000 | Allowed |
steamApps(first: 200) { followerHistory(first: 200) } | 40,000 | ~161,000 | Allowed |
steamApps(first: 400) { followerHistory(first: 400) } | 160,000 | ~642,000 | Allowed |
steamApps(first: 500) { followerHistory(first: 500) } | 250,000 | ~1,002,000 | Rejected |
steamApps(first: 1000) { followerHistory(first: 1000) } | 1,000,000 | ~4,004,000 | Rejected |
Current limits
| Limit | Value |
|---|---|
| Complexity | 800,000 max |
| Depth | 10 levels of nesting |
| Breadth | 100 field selections |
| Page size | Up to 1,000 items per ordinary connection (first/last); specialized connections can have lower limits, including 100 for demos and DLC |
Complexity response data
Responses that reach complexity analysis include the query's complexity score so you can monitor your usage. Earlier rejections, such as request-rate and concurrency limits, may omit this data.
Response extensions (in the JSON body):
{
"data": { ... },
"extensions": {
"queryComplexity": {
"score": 31411,
"limit": 800000
}
}
}
Response header: X-Query-Complexity: 31411
How to reduce complexity
-
Reduce
first/laston your connections — this is the biggest lever -
Paginate using
aftercursors instead of requesting large pages -
Flatten — requesting 1 app with 1,000 history entries is much cheaper than 100 apps with 100 history entries each
-
Separate queries — split large requests into multiple smaller ones (each query's complexity is calculated independently)
When a query is rejected
You'll receive a GraphQL error with the complexity score and limit:
{
"errors": [{
"message": "Query exceeds maximum complexity (complexity: 1002001, max: 800000)"
}],
"extensions": {
"queryComplexity": {
"score": 1002001,
"limit": 800000
}
}
}
Reduce the first/last values on your connections, especially nested ones.
Concurrency limit
The API limits the number of queries that can execute simultaneously, both per client and globally. This protects the database from being overwhelmed when many heavy queries arrive at the same time, and ensures one client can't monopolize all available capacity.
There are two layers:
-
Per-client limit — each API key can have at most a few queries executing at the same time. If you exceed this, you'll receive a
503with a message indicating your queries are the bottleneck. -
Global limit — the total number of concurrent queries across all clients is capped. If the server is at capacity due to combined load from multiple clients, you'll receive a
503.
When you receive a 503 Service Unavailable response, retry after a few seconds. These limits are transient — slots free up as queries complete.
Response headers:
-
X-Concurrent-Queries— current number of in-flight queries (global) -
X-Concurrent-Limit— maximum allowed concurrent queries (global) -
X-Concurrent-Queries-Self— current number of your in-flight queries -
X-Concurrent-Limit-Self— maximum allowed concurrent queries per client
Server-time budget
The server-time budget tracks how much cumulative server processing time your API key consumes within a fixed 15-minute window. This highlights usage patterns where individual queries are within complexity limits but collectively consume too many server resources.
Server-time budget is informational only until further notice.
How it works
Each API key has a budget of server processing time per window. Windows align to wall-clock quarter-hours, rather than rolling forward with each request. Query processing time counts toward this budget. Usage is tracked and reported, but exceeding the budget does not currently reject requests. Request-rate, complexity, and concurrency limits still apply independently.
Response data
Responses that reach the budget middleware include usage headers when tracking is available. GraphQL results include the budget extension when complexity and budget data are available. Responses rejected earlier, or requests where tracking is unavailable, may omit this data.
Response extensions (in the JSON body):
{
"data": { ... },
"extensions": {
"serverTimeBudget": {
"budgetSeconds": 300,
"usedSeconds": 42.5,
"resetSeconds": 279
}
}
}
Response headers:
-
X-Server-Time-Budget— total budget in seconds -
X-Server-Time-Used— server time used in current window (seconds) -
X-Server-Time-Reset— seconds until the current window resets
How to stay within budget
-
Use smaller page sizes —
steamApps(first: 100)is much faster thansteamApps(first: 1000) -
Reduce nested connection sizes —
ccuHistory(first: 90)instead ofccuHistory(first: 360) -
Space out requests — usage resets at the next fixed 15-minute window
-
Monitor your usage — check the
serverTimeBudgetextension when present
When you exceed the budget
Budget enforcement is currently disabled: exceeding the server-time budget alone does not produce a 429 response or require a retry. Use the usage data to reduce expensive queries and spread requests out. If you receive a request-rate 429, follow the separate request rate limit instructions.