Rate Limits
Klubero SSO implements rate limiting to ensure service stability and prevent abuse.
Rate limits table
All rate limits are applied per IP address.
| Endpoint | Limit | Window | Purpose |
|---|---|---|---|
/connect/token | 10 requests | 15 minutes | Protection against brute-force attacks on credentials |
/api/twofactor/* | 5 requests | 5 minutes | Protection against brute-force attacks on verification codes |
/api/magiclink/send | 3 requests | 5 minutes | Prevention of email spam |
/connect/authorize | 30 requests | 1 minute | Protection against DoS and redirect URI enumeration |
Response when the limit is exceeded
When a rate limit is exceeded, the server returns HTTP status 429 Too Many Requests with a Retry-After header (the number of seconds until the next allowed request) and a short plain-text body:
HTTP/1.1 429 Too Many Requests
Retry-After: 300
Content-Type: text/plain
API calls quota exceeded! maximum admitted 10 per 15m.
note
The response is not in JSON format. To determine the wait time, always read the value of the Retry-After header (in seconds); do not rely on parsing the response body. The exact wording of the text body may vary depending on the configuration.
Handling rate limits
- Check the response status: Look for HTTP 429
- Read the Retry-After header: It indicates the number of seconds to wait
- Implement exponential backoff: For automated systems
- Cache tokens: Reuse access tokens until they expire
Best practices
Do
- Cache and reuse tokens until they expire
- Use refresh tokens instead of re-authenticating
- Implement proper error handling with backoff
- Batch operations where possible
Don't
- Request a new token for every API call
- Re-authenticate for every session
- Retry immediately on failure
- Make many sequential requests
Example: Token caching
let tokenCache = {
accessToken: null,
expiresAt: null
};
async function getAccessToken() {
// Return the cached token if it's still valid (with a 60s buffer)
if (tokenCache.accessToken && tokenCache.expiresAt > Date.now() + 60000) {
return tokenCache.accessToken;
}
// Request a new token
const response = await fetch('/connect/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
})
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
throw new Error(`Rate limited. Try again in ${retryAfter} seconds.`);
}
const data = await response.json();
tokenCache.accessToken = data.access_token;
tokenCache.expiresAt = Date.now() + (data.expires_in * 1000);
return tokenCache.accessToken;
}