calcom/cal.diy · warning · ThrottlerException
CustomThrottlerGuard - Too many requests. Please try again l
Error message
CustomThrottlerGuard - Too many requests. Please try again later.
What it means
Thrown by CustomThrottlerGuard.handleApiEndpointThrottle when a route decorated with @Throttle exceeds its per-endpoint rate limit. The guard combines the tracker (API key hash, IP hash, OAuth client hash, or access token hash) with the throttle option name to form a unique rate-limit key, then increments a Redis counter. If the counter exceeds the limit during the TTL or block-duration window, this exception fires and NestJS returns HTTP 429.
Source
Thrown at apps/api/v2/src/lib/throttler-guard.ts:74
const request = context.switchToHttp().getRequest<Request>();
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
const response = context.switchToHttp().getResponse<Response>();
const tracker = await this.getTracker(request);
if (throttleOptions) {
return this.handleApiEndpointThrottle(tracker, throttleOptions, response);
}
if (tracker.startsWith("api_key_")) {
return this.handleApiKeyRequest(tracker, response);
} else {
return this.handleNonApiKeyRequest(tracker, response);
}
}
private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) {
const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response);
if (isBlocked) {
throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later.");
}
return true;
}
private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
let allLimitsBlocked = true;
for (const rateLimit of rateLimits) {
const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);
if (!isBlocked) {
allLimitsBlocked = false;
}
}
if (allLimitsBlocked) {
throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later.");View on GitHub (pinned to 176037d0af)
Solutions
- Inspect the X-RateLimit-Remaining-<Name> and X-RateLimit-Reset-<Name> response headers (set in incrementRateLimit at lines 192-198) to see remaining quota before retrying.
- Implement exponential backoff on the client: wait for the X-RateLimit-Reset value, then retry with increasing delay.
- If the limit is genuinely too low for a legitimate use case, adjust the @Throttle decorator parameters on the specific controller method or raise the corresponding env defaults.
- In test environments, clear the Redis keys matching the tracker pattern or use a separate Redis instance to avoid cross-test contamination.
Example fix
// before: client sends requests in a tight loop
for (const item of items) {
await api.post('/throttled-endpoint', item);
}
// after: respect rate-limit headers and back off
const makeRequest = async (item) => {
const res = await api.post('/throttled-endpoint', item);
const remaining = res.headers['x-ratelimit-remaining-custom'];
if (Number(remaining) <= 1) {
const resetMs = Number(res.headers['x-ratelimit-reset-custom']);
await new Promise(r => setTimeout(r, resetMs));
}
};
for (const item of items) {
await makeRequest(item);
} Defensive patterns
Strategy: retry
Validate before calling
// Before sending, check the last response's rate-limit headers
const canSend = (lastResponse: Response): boolean => {
const remaining = Number(lastResponse.headers.get('x-ratelimit-remaining-custom') ?? 1);
return remaining > 0;
};
if (!canSend(lastRes)) {
const resetMs = Number(lastRes.headers.get('x-ratelimit-reset-custom') ?? 60000);
await new Promise(r => setTimeout(r, resetMs));
} Try / catch
// Retry with exponential backoff respecting Retry-After
const callWithBackoff = async <T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> => {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err: any) {
const isThrottle = err?.response?.status === 429 || err?.name === 'ThrottlerException';
if (!isThrottle || attempt >= maxRetries) throw err;
const retryAfter = Number(err?.response?.headers?.['retry-after'] ?? Math.pow(2, attempt));
await new Promise(r => setTimeout(r, retryAfter * 1000));
attempt++;
}
}
}; Prevention
- Read and cache the X-RateLimit-Remaining-* and X-RateLimit-Reset-* headers from every response to throttle client-side before hitting the server limit.
- Use a token-bucket or leaky-bucket rate limiter in your HTTP client (e.g. p-limit, bottleneck) sized to the known endpoint limit.
- In integration tests, use a separate Redis instance or flush rate-limit keys between test suites to avoid cross-contamination.
- Cache responses client-side to reduce redundant calls to throttled endpoints.
When it happens
Trigger: A request hits a controller method decorated with @Throttle(name, limit, ttl, blockDuration). The same tracker (e.g. api_key_<sha256>) sends more than 'limit' requests within 'ttl' milliseconds, or is still within the 'blockDuration' window after a previous violation. This path is taken only when throttleOptions is non-null (line 60-61), bypassing the generic API-key and non-API-key rate limit handlers.
Common situations: Tight per-endpoint throttle limits set too low for legitimate burst traffic. Integration tests hammering a throttled endpoint without resetting Redis. A webhook receiver or polling client exceeding the decorated endpoint's custom limit. Environment variable RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS set very high, causing prolonged blocks.
Understand the failure class
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- CustomThrottlerGuard - Invalid API Key
- ApiKeysService - This endpoint can only be accessed using an
- ApiKeysService - No API key provided
- ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNe
- ApiKeysService - provided api key is not valid.
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/ebadec69ba85bd9f.
Report an issue: GitHub.