signalapp/Signal-Server · warning
return Response.status(429).build();
Error message
return Response.status(429).build();
What it means
The push-challenge endpoint (POST /v1/challenge/push) returns HTTP 429 when the challenge constraint check says push challenges are not permitted for this account. ChallengeConstraintChecker aggregates rate limits, CAPTCHA/registration-lock state, and abuse heuristics; pushPermitted()==false means the server has decided this account has requested too many push challenges or must satisfy another challenge first. It is a deliberate throttle, not a crash.
Solutions
- Stop retrying and honor the 429; wait before the next push-challenge request
- Check the rate-limit configuration (challenge rate limiter TTLs/limits) if legitimate traffic is throttled
- Complete any alternative challenge (e.g. CAPTCHA) required before requesting push challenges again
- Inspect ChallengeConstraintChecker wiring if constraints are unexpectedly false in a deployment
Example fix
// before: immediate retry on failure
sendPushChallenge(account);
// after: back off on 429
if (response.code() == 429) {
long retryAfter = parseRetryAfter(response);
scheduler.schedule(this::sendPushChallenge, retryAfter, TimeUnit.SECONDS);
} Defensive patterns
Strategy: retry
Validate before calling
// client-side: check local request history before calling
if (lastPushChallengeAt != null && Duration.between(lastPushChallengeAt, Instant.now()).toMinutes() < COOLDOWN_MINUTES) {
return; // skip; server will 429 otherwise
} Try / catch
// on 429, schedule a delayed retry instead of immediate resend
if (response.code() == 429) {
backoffScheduler.schedule(this::requestPushChallenge, cooldown(), TimeUnit.SECONDS);
} Prevention
- Rate-limit push-challenge requests locally on the client
- Solve required CAPTCHAs before requesting push challenges
- Avoid shared/flagged IPs when testing
- Honor Retry-After-style backoff instead of tight retry loops
When it happens
Trigger: Client calls requestPushChallenge for an account whose combined constraints (per-IP, per-account rate limits, unmet CAPTCHA requirements) yield pushPermitted()=false. Repeatedly requesting push challenges for the same account or IP within the rate-limit window triggers it.
Common situations: Mobile clients auto-retrying push-challenge requests after message-send failures; load tests or shared IPs (NAT, VPN) exhausting per-IP quotas; an account stuck in a state requiring CAPTCHA but the client never solving it and re-requesting pushes.
Related errors
- return Response.status(404).build();
- return Response.status(411)
- return Response.status(503).build();
- return Response.status(422).build();
- Only primary devices may register attestations
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/84ab78b3533dc991.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/ChallengeController.java:193
@ApiResponse(responseCode = "404", description = """
The server does not have a push notification token for the authenticated account’s main device; clients may add a push
token and try again
""")
@ApiResponse(responseCode = "413", description = "Too many attempts", headers = @Header(
name = "Retry-After",
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
name = "Retry-After",
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
public Response requestPushChallenge(@Auth final AuthenticatedDevice auth,
@Context ContainerRequestContext requestContext) {
final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier())
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
final ChallengeConstraints constraints = challengeConstraintChecker.challengeConstraintsHttp(requestContext, account);
if (!constraints.pushPermitted()) {
return Response.status(429).build();
}
try {
rateLimitChallengeManager.sendPushChallenge(account);
return Response.status(200).build();
} catch (final NotPushRegisteredException e) {
return Response.status(404).build();
}
}
}
View on GitHub (pinned to 100ab61c82)