signalapp/Signal-Server · warning · ClientErrorException
429 Too Many Requests (rate limit exceeded)
Error message
429 Too Many Requests (rate limit exceeded)
What it means
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.
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).
Defensive patterns
Strategy: retry
Try / catch
try { await updateSession(...); } catch (e) {
if (e.status === 429) { await sleep(e.body.retryAfterMs ?? backoff()); return updateSession(...); }
throw e;
} Prevention
- Exponential backoff with jitter on 429
- Don't poll updateSession in tight loops
- Disable/raise limits in test environments
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- 429 Too Many Requests or 409 Conflict (not allowed to…
- 403 Forbidden
- Encountered a negative retry duration
- too few parts
- invalid captcha scheme
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/e40eb2a43fc6ce4f.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java:311
updateVerificationSessionRequest);
try {
// these handle* methods ordered from least likely to fail to most, so take care when considering a change
verificationSession = verificationCheck.updatedSession().orElse(verificationSession);
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
verificationSession = handlePushChallenge(updateVerificationSessionRequest, registrationServiceSession,
verificationSession);
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
verificationSession, userAgent, verificationCheck.scoreThreshold());
} catch (final RateLimitExceededException e) {
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
e.getRetryDuration());
throw new ClientErrorException(response);
} catch (final ForbiddenException e) {
throw new ClientErrorException(Response.status(Response.Status.FORBIDDEN)
.entity(buildResponse(registrationServiceSession, verificationSession))
.build());
} finally {
// Each of the handle* methods may update requestedInformation, submittedInformation, and allowedToRequestCode,
// and we want to be sure to store a changes, even if a later method throws
verificationSessionManager.update(verificationSession);
}
return buildResponse(registrationServiceSession, verificationSession);
}
/**
* If {@code pushTokenAndType} values are not {@code null}, sends a push challenge. If there is no existing pushView on GitHub (pinned to 100ab61c82)