{"record":{"id":"e40eb2a43fc6ce4f","repo":"signalapp/Signal-Server","slug":"429-too-many-requests-rate-limit-exceeded","errorCode":null,"errorMessage":"429 Too Many Requests (rate limit exceeded)","messagePattern":"429 Too Many Requests \\(rate limit exceeded\\)","errorType":"http","errorClass":"ClientErrorException","httpStatus":429,"severity":"warning","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java","lineNumber":311,"sourceCode":"        updateVerificationSessionRequest);\n\n    try {\n      // these handle* methods ordered from least likely to fail to most, so take care when considering a change\n\n      verificationSession = verificationCheck.updatedSession().orElse(verificationSession);\n\n      verificationSession = handlePushToken(pushTokenAndType, verificationSession);\n\n      verificationSession = handlePushChallenge(updateVerificationSessionRequest, registrationServiceSession,\n          verificationSession);\n\n      verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,\n          verificationSession, userAgent, verificationCheck.scoreThreshold());\n    } catch (final RateLimitExceededException e) {\n\n      final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,\n          e.getRetryDuration());\n      throw new ClientErrorException(response);\n\n    } catch (final ForbiddenException e) {\n\n      throw new ClientErrorException(Response.status(Response.Status.FORBIDDEN)\n          .entity(buildResponse(registrationServiceSession, verificationSession))\n          .build());\n\n    } finally {\n      // Each of the handle* methods may update requestedInformation, submittedInformation, and allowedToRequestCode,\n      // and we want to be sure to store a changes, even if a later method throws\n      verificationSessionManager.update(verificationSession);\n    }\n\n    return buildResponse(registrationServiceSession, verificationSession);\n  }\n\n  /**\n   * If {@code pushTokenAndType} values are not {@code null}, sends a push challenge. If there is no existing push","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java#L293-L329","documentation":"updateSession converts a RateLimitExceededException from handleCaptcha (or other handle* checks) into an HTTP 429 ClientErrorException carrying a response built by buildResponseForRateLimitExceeded, which includes the retry duration. The registration service rate-limits verification attempts (captcha assessments, code requests) per number/session to prevent abuse. The client must wait until the indicated retry time before retrying.","triggerScenarios":"PUT/PATCH to the verification session endpoint when handleCaptcha (or another handler) hits a rate limiter — e.g. repeated failed captcha submissions or too many session updates for one phone number within the limiter window.","commonSituations":"Automated tests or scripts hammering the verification endpoint with the same number; a user repeatedly failing captcha; load testing without mocking the rate limiter; shared infrastructure (NAT/proxy) making many users share one rate-limit key.","solutions":["Read the retry duration from the 429 response body and wait until then before retrying.","Back off exponentially and add jitter to verification retries in your client.","Reduce redundant updateSession calls — batch or cache state instead of polling.","For tests, use a test/dev profile that disables or raises the rate limits (rate limiters configuration)."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await updateSession(...); } catch (e) {\n  if (e.status === 429) { await sleep(e.body.retryAfterMs ?? backoff()); return updateSession(...); }\n  throw e;\n}","preventionTips":["Exponential backoff with jitter on 429","Don't poll updateSession in tight loops","Disable/raise limits in test environments"],"tags":["rate-limit","http-429","captcha","verification"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}