Skip to content

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.

TierEndpointsLimitScopeEnforcement
Public/api/public/*100 req/minPer IPCloudflare WAF
Operator/api/operator/*30 req/minPer IPCloudflare WAF
Admin/api/admin/*10 req/minPer IPCloudflare WAF

Operator Player API Rate Limits

Enforced by Express middleware on the operator's backend (apps/operator-api/).

TierEndpointsLimitScopeEnforcement
Auth/api/auth/register, /api/auth/login5 req/minPer IPExpress middleware
Auth refresh/api/auth/refresh20 req/minPer IPExpress middleware
Player readGET /api/player/*60 req/minPer playerExpress middleware
Player writePOST /api/player/*20 req/minPer playerExpress middleware
Ticket purchasePOST /api/player/tickets/purchase10 req/minPer playerExpress middleware
Admin/api/admin/*30 req/minPer API keyExpress middleware

Response Headers

Every API response includes rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1708099260
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix 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

  1. Cache public data — Jackpot, odds, and history responses change infrequently. Cache for 30-60 seconds.
  2. Batch ticket submissions — Submit tickets in bulk using the array format in POST /api/operator/tickets rather than one-at-a-time.
  3. Use webhooks — Don't poll for draw results. Register a webhook URL and receive push notifications.
  4. Respect backoff — When you receive a 429, wait the indicated time before retrying.
  5. Use the lightweight counter — For real-time jackpot display, use /api/public/jackpot-counter instead of /api/public/jackpot.