CEEJAYDocs

Rate limits

Each API key can make 60 requests in a one-minute window, and every response says how many are left.

The default is 60 requests per minute per key. A higher limit can be set per key, up to 600 a minute: talk to us if your integration needs more. The dashboard has no setting for it. A key whose limit was set above 600 is capped at 600.

The headers on every response

Every response to a request made with a known key includes the key's rate limit status:

HeaderMeaning
X-RateLimit-LimitThe key's requests per minute
X-RateLimit-RemainingRequests left in the current window. Never below 0
X-RateLimit-ResetWhole seconds until the window ends, from 1 to 60
200 OK
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 42
X-Request-Id: req_5f0c2a9b7d3e41f8a6b0c1d2

They are on a success, on a 400, on a 403 for a missing scope, on a 429 and on a 500. Community details reports the same limit as key.rateLimitPerMinute.

These responses include none of the three:

  • A 401 for a secret key. No key is known, so there is no limit to report. It does carry X-Request-Id. The requests a game makes are counted per IP address, so there a 401 includes all three: see Connect.
  • A response that comes before CEEJAY's own code runs: a body that is not valid JSON, a body over 1 MB, a path that does not exist, and the redirect for a path that ends in a slash.

How the window works

The limit belongs to the key. Two keys have two separate budgets, whatever IP address they call from.

The window is fixed: it opens with the first request the key makes and lasts 60 seconds. The next request after that opens a new window. So a burst at the end of one window and another at the start of the next can add up to more than the limit in a short time. Treat the limit as a ceiling, not a target.

Every request made with a valid key is counted, whatever the response turns out to be, including 400, 403, 404, 405 and 500. That includes a request refused because the key lacks the scope the request needs: the limit is checked before the scope, so a key that is over its limit gets 429 from every request. A request refused for a bad key is not counted, and neither is one whose body is not valid JSON.

A key CEEJAY does not recognise is not counted against any key, but it is counted against the IP address it came from. After 60 unknown keys from one IP address within a minute, that IP address receives 429 with the code rate_limited for the rest of the minute, before the key is looked up, with retryAfter and a Retry-After header and none of the three limit headers. A key CEEJAY recognised moments ago still goes through. This protects against key guessing; a server using its own key is not affected.

CEEJAY stores the count in a shared store, so the limit is exact and persists across deployments. On an instance without that shared store, or while it cannot be reached, each of CEEJAY's processes counts on its own and starts again when it restarts: no request is refused because of it, and in such a minute a key can get up to its limit from each.

Connect

The requests of Connect are counted in their own way, and none of them uses up a key's requests per minute.

Connect requests from your server

The five requests a server makes with a secret key (Create a connect pass and the four connect session requests) share one limit: 600 a minute for each game. A server that polls many open sessions would use up a key's 60 in seconds, so these are not counted against the key.

One key also has an abuse limit of 6,000 a minute across them. It applies to calls that never reach a game, such as one refused for a missing scope, and a key that serves several games within their own limits does not reach it.

The three headers report which count the response was made under:

The responseX-RateLimit-Limit
Returned once the request found its game: a success, a refusal of the request itself600, for the game
Returned before that: a missing scope, a wrong method, an instance without Connect, a game that is not found or that the key cannot use6000, for the key

Over either one the response is 429 with the code rate_limited, retryAfter and a Retry-After header. The game's count is stored in CEEJAY's database, so it persists across deployments.

Requests a game makes

The seven requests of Connect that a game makes with its public key or a player session, and the eleven requests of the game loop, are counted per IP address: 600 a minute, across all eighteen. The three headers report that count, on every response, a 401 included. An IPv6 address is counted as its /64.

Some have a limit of their own on top:

RequestIts own limitOver it
Start connect600 starts from one IP address in 10 minutes. 10 starts with verified proof for one player in 10 minutes. 20 attempts with an unknown code from one IP address in 10 minutes429 with the code too_many_attempts
Get the resultOne read for each session every 2 seconds. Never limited per IP address beyond the 600429 with the code rate_limited
Get connection status60 a minute from one IP address429 with the code rate_limited
Renew a player session10 a minute for one device429 with the code rate_limited
Get the player's profile140 a minute for one player session, shared with the requests of the game loop. Its headers report this count429 with the code rate_limited

Every one of these responses includes retryAfter and a Retry-After header.

When more than 200 unknown codes arrive with one public key in 10 minutes, codes sent with that key slow down until the window ends: each response is delayed by 2 seconds, and an IP address gets 2 attempts instead of 20. The key is never switched off, so nobody with a shipped key can stop other players from connecting.

These numbers are ceilings against abuse, set well above what players behind one shared IP address need. A studio's own server has one IP address: it uses the requests under Connect requests from your server, never these.

The game loop

The eleven requests of the game loop are counted per IP address with the others, and also per player session, per member and per game. Every one of these responses includes retryAfter and a Retry-After header.

What is countedLimitOver it
One IP address, across every request a game makes600 a minute429 with the code rate_limited
One player session, any request, Get the player's profile included140 a minute429 with the code rate_limited
One player session, reads: List the quests, List the shop, Read the locker, Get the ranks and Get the balance120 a minute429 with the code rate_limited
One player session, writes: Start a quest, Verify and Answer a quest task, Buy a shop item, Claim an item and Check in20 a minute429 with the code rate_limited
One member in one game, buying and claiming together60 an hour429 with the code rate_limited
One game, every request of the loop together12,000 a minute429 with the code rate_limited

The headers report the count of the kind of request once it is counted: 120 for a read and 20 for a write. A request refused before that, by one of the gates of the loop or by a check on its body, reports the player session's 140. A repeat of Buy a shop item that returns the stored response for its idempotencyKey is not counted against any of these limits.

The last limit is an abuse limit and is counted only after the player session is read, so a flood of bad tokens cannot use it up. The daily limit on the coins a member can spend is not a rate limit: the game loop explains it.

Player requests from your server

The eight requests about players and the field map (Get a player, List players, Disconnect a player, Delete a player, Set player fields, Log player events, Get the field map and Propose to the field map) are counted against the key's own requests per minute, like any request outside Connect. The headers report the key's limit.

A call that carries up to 500 players or events is one request, so 60 calls a minute can carry 30,000 players. Alongside the key's number, Log player events has a ceiling for the game: 3,000 events a minute unless another number was set for it. Above it the whole call is refused with 429, the code event_rate_exceeded, retryAfter and a Retry-After header, and nothing is stored. A call that is over the ceiling counts too, so one that keeps going stays refused until the minute ends. That 429 includes the key's headers: X-RateLimit-Limit does not report the ceiling.

A body over 512 KB on Set player fields and Log player events is refused with 413 and the code body_too_large, in the usual error shape, with the three headers: it is counted like any other request.

When you are limited

Exceeding the limit returns 429 with the code rate_limited and a Retry-After header giving the seconds until the window resets. A limited request does nothing, so it is safe to retry.

429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 34
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 34
X-Request-Id: req_5f0c2a9b7d3e41f8a6b0c1d2

{
  "error": "Rate limit exceeded",
  "code": "rate_limited",
  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
  "retryAfter": 34
}

Retry-After, X-RateLimit-Reset and retryAfter carry the same number.

Handling limits well

  • Read X-RateLimit-Remaining and slow down before it reaches 0.
  • Honour Retry-After rather than retrying immediately.
  • Back off exponentially with jitter if you retry repeatedly.
  • Batch where you can: award XP once at the end of a session rather than per action.
  • Use webhooks instead of polling. Events such as member.joined are delivered without using any of your request limit.

Discord's limits

Requests that act on Discord also pass through Discord's own rate limits: Grant a role, Revoke a role, Send a message, List roles and List channels. When Discord limits CEEJAY's bot, you receive a 429 too, with the code discord_rate_limited:

429 from Discord
{
  "error": "Discord API rate limited, try again later",
  "code": "discord_rate_limited",
  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
  "retryAfter": 3
}

retryAfter, and the Retry-After header with it, contain Discord's retry time in whole seconds, at most 60. Both are omitted when Discord gave no retry time: wait a few seconds, and back off if it happens again.

Tell the two apart by the code. A 429 from Discord does not mean your key's own window is used up: X-RateLimit-Remaining is not 0 unless it is.

A limit Discord sets for one channel or one server stops CEEJAY's calls for that channel or server only, until Discord's retry time has passed. The limit Discord sets for the bot as a whole stops every request that calls Discord, and a request that checks a member with Discord then returns 503 with the code discord_unavailable.

Do not retry Discord-backed calls in a tight loop. Sustained retries can get the bot temporarily banned from the Discord API, which affects every feature, not only yours.

What is not counted here

Webhook deliveries are not limited by this. AI agents connected over MCP have their own limits, set per community.

On this page

Community StudiosPrivacyTerms