Concepts
Rate limits
How many requests each key, player and address may send per period, what a 429 looks like, and how to stay under the limits.
On this page
The limits
Each limit counts requests in a fixed window. The window starts at the top of the period (the top of the minute or of the hour) and the count restarts when it ends.
| What is counted | Limit | Window |
|---|---|---|
| Server API, per secret key | 600 requests | 1 minute |
| Client API with a player token, per player | 120 requests | 1 minute |
| Client API without a token (catalog, token exchange), per IP address | 120 requests | 1 minute |
| Creating an anonymous player, per IP address | 30 requests | 1 hour |
| Gift code attempts (right or wrong), per player | 10 attempts | 15 minutes |
| Gift code attempts, client API, per IP address | 30 attempts | 15 minutes |
| Creator codes typed at checkout, per player | 30 attempts | 15 minutes |
| Creator codes typed at checkout, client API, per IP address | 60 attempts | 15 minutes |
Two keys do not share a counter, and neither do two players. A busy player cannot slow down the others.
The OpenAPI document (/api/v1/openapi.json) is public and has no rate limit.
When you go over
The request is refused with 429 RATE_LIMITED and nothing changes. The Retry-After header holds the number of seconds to wait.
HTTP/1.1 429 Too Many Requests
Retry-After: 33
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
{"error":{"code":"RATE_LIMITED","message":"Too many requests"}}The order of checks matters: a request is checked for authentication first, then the rate limit, then the idempotency key, then the body. A refused request still counts.
Staying under the limits
- Do not poll. Read a balance when something may have changed, not on a timer. The browser SDK keeps a cached copy:
gc.balance("gems")costs no request, andchangeevents tell you when it moves. - Cache the catalog. It changes when you edit it, not on every frame.
- Use one token per player session, and renew it only when it expires (once an hour).
- Spread bulk work. A job that grants rewards to thousands of players should pace itself under 600 requests a minute, with a small queue.
Retrying a 429
Wait for Retry-After, then send the same request again. If it is a write, send it with the same idempotency key: the retry cannot count twice.
The SDKs do it for you. The server SDKs retry a 429 after Retry-After (up to 30 seconds), up to two times by default. The browser SDK waits up to 10 seconds; for a longer wait it stops and raises RATE_LIMITED, with the seconds in error.details.retryAfter, so a game never freezes.
import { ErrorCode, GameCoinError } from "@apilow/gamecoin";
try {
await gamecoin.catalog.get();
} catch (error) {
// The SDK already waited and retried; this is what is left.
if (error instanceof GameCoinError && error.code === ErrorCode.RATE_LIMITED) {
console.log(`still limited: try again in ${error.retryAfter} seconds`);
} else {
throw error;
}
}from gamecoin import ErrorCode, GameCoinError
try:
gamecoin.catalog.get()
except GameCoinError as error:
# The SDK already waited and retried; this is what is left.
if error.code == ErrorCode.RATE_LIMITED:
print(f"still limited: try again in {error.retry_after} seconds")
else:
raiseuse Apilow\GameCoin\ErrorCode;
use Apilow\GameCoin\GameCoinException;
try {
$gamecoin->catalog->get();
} catch (GameCoinException $e) {
// The SDK already waited and retried; this is what is left.
if ($e->errorCode === ErrorCode::RATE_LIMITED) {
echo "still limited: try again in {$e->retryAfter} seconds\n";
} else {
throw $e;
}
}_, err = client.Catalog.Get(ctx)
var gcErr *gamecoin.Error
switch {
case errors.As(err, &gcErr) && gcErr.Code == gamecoin.CodeRateLimited:
// The SDK already waited and retried; this is what is left.
fmt.Printf("still limited: try again in %.0f seconds\n", gcErr.RetryAfter.Seconds())
case err != nil:
log.Fatal(err)
}using Apilow.GameCoin;
try
{
await gamecoin.Catalog.GetAsync();
}
catch (GameCoinException ex) when (ex.Code == GameCoinErrorCodes.RateLimited)
{
// The SDK already waited and retried; this is what is left.
Console.WriteLine($"still limited: try again in {ex.RetryAfter?.TotalSeconds} seconds");
}try {
await gc.refresh();
} catch (error) {
if (error.code === "RATE_LIMITED") {
console.log(`try again in ${error.details.retryAfter} seconds`);
} else {
throw error;
}
}Where next
- Idempotency: how to retry a write safely.
- Errors