Rate limiting
Every authenticated request to the machine API passes two counters before it runs. The limits below are the current production-source defaults for newly issued live keys. Legacy test credentials have separate lower limits; their existence does not enable new test-key issuance.
Limits
Section titled “Limits”| Bucket | Scope field | Limit |
|---|---|---|
| All requests, per API key | key |
600 / minute |
| All requests, per issuing user, summed across that user’s keys | user |
3000 / minute |
| Failed authentication attempts, per source IP | none (separate counter) | 100 / minute |
Both request buckets are evaluated over a fixed 60-second window: the counter increments per request and uses an epoch-aligned minute boundary. Every request counts against both buckets; there is no separate budget per method, so reads and writes draw from the same counters. The failed-authentication counter protects the login surface against brute force and only counts requests whose key was rejected.
Plan aggregate traffic across workers and keys. If legitimate demand exceeds the limits, discuss capacity with support before changing your traffic pattern; do not assume a self-service per-account override exists.
Step check: fire 10 sequential GET /v1/games?per_page=1 requests and watch X-RateLimit-Remaining decrease with each response; that is the per-key bucket counting you down.
Response signals
Section titled “Response signals”Responses that expose rate-limit headers report the bucket state; not every route propagates them. Handle their absence. A representative header set is:
HTTP/1.1 200 OKX-RateLimit-Limit: 600X-RateLimit-Remaining: 583X-RateLimit-Reset: 1756110000X-RateLimit-Reset is a Unix timestamp computed as response time plus 60 seconds, an upper bound rather than the exact fixed-window boundary. When a bucket is exhausted, the request is rejected with 429 before it executes:
HTTP/1.1 429 Too Many RequestsRetry-After: 60X-RateLimit-Limit: 600X-RateLimit-Remaining: 0X-RateLimit-Reset: 1756110000Content-Type: application/json{"error": "rate_limit_exceeded", "scope": "key", "retry_after": 60}The scope field names the bucket that tripped: key for your API key’s own counter, user for the issuing-user counter shared by that user’s keys. The rate-limit rejection body is this compact object rather than the standard error envelope; in the error reference the condition is catalogued as E9001. This authentication-layer rejection happens before the handler. Preserve the original operation key and body when retrying; other 429 sources have their own semantics.
Step check: your client treats any 429 as “wait, then retry the identical request”, keyed off the headers rather than a hard-coded sleep.
Backoff
Section titled “Backoff”- Honour
Retry-Afterfirst. It is seconds;60means the window that rejected you is at most a minute from rolling over. Alternatively wait untilX-RateLimit-Reset. - Add jitter when many workers share a key. If every worker sleeps exactly 60 seconds, they all return in the same second and trip the bucket again. Randomise within a few seconds.
- Watch
X-RateLimit-Remainingproactively. Bulk jobs (catalog syncs, reconciliation sweeps) should throttle when remaining drops low instead of running into the wall. - Know which bucket you hit. Throttle the affected workers for
scope: "key"; coordinate all keys belonging to the issuer forscope: "user". Do not rotate or add keys to evade pacing. - Reserve capacity for player-facing launches when scheduling bulk catalog and reconciliation jobs.
Step check: use synthetic 429 responses to test both scopes, present/missing headers, jitter and bounded retries. Do not generate a production request burst merely to test client backoff.
Limiter availability
Section titled “Limiter availability”In production the limiter is backed by Redis and fails closed: if the rate-limiting backend is unavailable, authenticated requests are rejected with 503 rather than admitted uncounted:
{"error": "rate_limit_unavailable", "message": "Rate limiting backend unavailable"}That response carries Retry-After: 5. Treat it as transient platform degradation: back off a few seconds and retry, the same as any E0002-class condition. The platform prefers a brief availability dip over an unmetered abuse window on the money API.
Endpoint-specific limits
Section titled “Endpoint-specific limits”Beyond the two request buckets, specific operations carry their own guards, surfaced as 429 with a descriptive body:
- Free-round issuance is capped per player, per game, per 24 hours; grants beyond the cap return
429with"error": "rate_limited"and a message naming the 24-hour window. The source default is 50 grants per player and game over the last 24 hours, configurable by the service environment. Plan campaigns against the actual configured cap. Missing idempotency headers are rejected; retries must reuse the original key and body. - Concurrent sessions have an operator-level cap: exceeding it returns
E3003(session_limit_exceeded, 429). Complete or let old sessions expire before opening new ones. - Provider-side launch limits surface as
E1052(provider_launch_rate_limited, 429). That one is the provider’s own limit, not Aggregator.gg’s: wait and retry.
Step check: your alerting distinguishes the three 429 shapes, request buckets, endpoint guards, and provider-side limits, because the right reaction differs: back off, fix the loop, or just wait.