{"record":{"id":"ebadec69ba85bd9f","repo":"calcom/cal.diy","slug":"customthrottlerguard-too-many-requests-please-t","errorCode":null,"errorMessage":"CustomThrottlerGuard - Too many requests. Please try again later.","messagePattern":"CustomThrottlerGuard - Too many requests\\. Please try again later\\.","errorType":"exception","errorClass":"ThrottlerException","httpStatus":429,"severity":"warning","filePath":"apps/api/v2/src/lib/throttler-guard.ts","lineNumber":74,"sourceCode":"    const request = context.switchToHttp().getRequest<Request>();\n    const IP = request?.headers?.[\"cf-connecting-ip\"] ?? request?.headers?.[\"CF-Connecting-IP\"] ?? request.ip;\n    const response = context.switchToHttp().getResponse<Response>();\n    const tracker = await this.getTracker(request);\n    if (throttleOptions) {\n      return this.handleApiEndpointThrottle(tracker, throttleOptions, response);\n    }\n\n    if (tracker.startsWith(\"api_key_\")) {\n      return this.handleApiKeyRequest(tracker, response);\n    } else {\n      return this.handleNonApiKeyRequest(tracker, response);\n    }\n  }\n\n  private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) {\n    const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response);\n    if (isBlocked) {\n      throw new ThrottlerException(\"CustomThrottlerGuard - Too many requests. Please try again later.\");\n    }\n\n    return true;\n  }\n\n  private async handleApiKeyRequest(tracker: string, response: Response): Promise<boolean> {\n    const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);\n\n    let allLimitsBlocked = true;\n    for (const rateLimit of rateLimits) {\n      const { isBlocked } = await this.incrementRateLimit(tracker, rateLimit, response);\n      if (!isBlocked) {\n        allLimitsBlocked = false;\n      }\n    }\n\n    if (allLimitsBlocked) {\n      throw new ThrottlerException(\"CustomThrottlerGuard - Too many requests. Please try again later.\");","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/lib/throttler-guard.ts#L56-L92","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: client sends requests in a tight loop\nfor (const item of items) {\n  await api.post('/throttled-endpoint', item);\n}\n\n// after: respect rate-limit headers and back off\nconst makeRequest = async (item) => {\n  const res = await api.post('/throttled-endpoint', item);\n  const remaining = res.headers['x-ratelimit-remaining-custom'];\n  if (Number(remaining) <= 1) {\n    const resetMs = Number(res.headers['x-ratelimit-reset-custom']);\n    await new Promise(r => setTimeout(r, resetMs));\n  }\n};\nfor (const item of items) {\n  await makeRequest(item);\n}","handlingStrategy":"retry","validationCode":"// Before sending, check the last response's rate-limit headers\nconst canSend = (lastResponse: Response): boolean => {\n  const remaining = Number(lastResponse.headers.get('x-ratelimit-remaining-custom') ?? 1);\n  return remaining > 0;\n};\nif (!canSend(lastRes)) {\n  const resetMs = Number(lastRes.headers.get('x-ratelimit-reset-custom') ?? 60000);\n  await new Promise(r => setTimeout(r, resetMs));\n}","typeGuard":null,"tryCatchPattern":"// Retry with exponential backoff respecting Retry-After\nconst callWithBackoff = async <T>(fn: () => Promise<T>, maxRetries = 5): Promise<T> => {\n  let attempt = 0;\n  while (true) {\n    try {\n      return await fn();\n    } catch (err: any) {\n      const isThrottle = err?.response?.status === 429 || err?.name === 'ThrottlerException';\n      if (!isThrottle || attempt >= maxRetries) throw err;\n      const retryAfter = Number(err?.response?.headers?.['retry-after'] ?? Math.pow(2, attempt));\n      await new Promise(r => setTimeout(r, retryAfter * 1000));\n      attempt++;\n    }\n  }\n};","preventionTips":["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."],"tags":["rate-limiting","throttling","nestjs","api-v2","redis"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}