Appearance
Rate Limits
UltimaLotto applies rate limiting at two levels: Cloudflare WAF (network edge) and application middleware (API server).
Coordinator API Rate Limits
Enforced by Cloudflare WAF on api.ultimalotto.com.
| Tier | Endpoints | Limit | Scope | Enforcement |
|---|---|---|---|---|
| Public | /api/public/* | 100 req/min | Per IP | Cloudflare WAF |
| Operator | /api/operator/* | 30 req/min | Per IP | Cloudflare WAF |
| Admin | /api/admin/* | 10 req/min | Per IP | Cloudflare WAF |
Operator Player API Rate Limits
Enforced by Express middleware on the operator's backend (apps/operator-api/).
| Tier | Endpoints | Limit | Scope | Enforcement |
|---|---|---|---|---|
| Auth | /api/auth/register, /api/auth/login | 5 req/min | Per IP | Express middleware |
| Auth refresh | /api/auth/refresh | 20 req/min | Per IP | Express middleware |
| Player read | GET /api/player/* | 60 req/min | Per player | Express middleware |
| Player write | POST /api/player/* | 20 req/min | Per player | Express middleware |
| Ticket purchase | POST /api/player/tickets/purchase | 10 req/min | Per player | Express middleware |
| Admin | /api/admin/* | 30 req/min | Per API key | Express middleware |
Response Headers
Every API response includes rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1708099260| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Rate Limit Exceeded (HTTP 429)
When a rate limit is exceeded, the API returns:
json
{
"error": "Rate limit exceeded"
}Retry Strategy
Use exponential backoff when receiving 429 responses:
typescript
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const resetTime = response.headers.get("X-RateLimit-Reset");
const waitMs = resetTime
? (parseInt(resetTime) * 1000 - Date.now())
: Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, Math.max(waitMs, 1000)));
continue;
}
return response;
}
throw new Error("Rate limit exceeded after max retries");
}Best Practices
- Cache public data — Jackpot, odds, and history responses change infrequently. Cache for 30-60 seconds.
- Batch ticket submissions — Submit tickets in bulk using the array format in
POST /api/operator/ticketsrather than one-at-a-time. - Use webhooks — Don't poll for draw results. Register a webhook URL and receive push notifications.
- Respect backoff — When you receive a 429, wait the indicated time before retrying.
- Use the lightweight counter — For real-time jackpot display, use
/api/public/jackpot-counterinstead of/api/public/jackpot.