signalapp/Signal-Server · warning · ClientErrorException

429 Too Many Requests or 409 Conflict (not allowed to…

Error message

429 Too Many Requests or 409 Conflict (not allowed to request code)

What it means

requestVerificationCode throws this ClientErrorException when verificationSession.allowedToRequestCode() is false. The status depends on session state: if requestedInformation is empty (no challenge info outstanding) it returns 429 TOO_MANY_REQUESTS — too many code requests; otherwise 409 CONFLICT — required information (captcha, push challenge) has not been submitted yet. The body carries the session state including any requestedInformation.

Solutions

  1. Read the response body: if requestedInformation lists CAPTCHA or PUSH_CHALLENGE, call updateSession with those proofs first, then re-request the code.
  2. For the 429 path, wait for the indicated duration before requesting another code; respect any retryAfter hints.
  3. Reduce code-request frequency in client retry logic (backoff, no tight loops).
  4. Create a fresh verification session if the current one is permanently disallowed.

Example fix

// before
await requestVerificationCode(sessionId, {transport: 'sms'});
// after
const s = await getSession(sessionId);
if (!s.allowedToRequestCode && s.requestedInformation.includes('CAPTCHA')) {
  await updateSession(sessionId, {captcha: token});
}
await requestVerificationCode(sessionId, {transport: 'sms'});
Defensive patterns

Strategy: validation

Validate before calling

const s = await getSession(sessionId);
if (!s.allowedToRequestCode) {
  if (s.requestedInformation.length) await completeChallenges(s.requestedInformation);
  else throw new Error('wait before requesting another code');
}

Type guard

function canRequestCode(s) { return s.allowedToRequestCode === true; }

Try / catch

try { await requestVerificationCode(...); } catch (e) {
  if (e.status === 409) await completeChallenges(e.body.session.requestedInformation);
  else if (e.status === 429) await sleep(e.body.retryAfterMs ?? 60000);
  else throw e;
}

Prevention

When it happens

Trigger: POSTing a code request when (a) the per-session/per-number code-request rate limiter already flagged the session (429 path), or (b) requestedInformation is non-empty — e.g. captcha or push challenge still pending — so the session disallows code delivery (409 path).

Common situations: Rapid re-requesting of SMS/voice codes hitting attempt caps; clients skipping updateSession (captcha/push) before requesting a code; region-specific enforcement demanding captcha that the client ignored; shared IPs amplifying rate-limit hits.

Understand the failure class

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/6f82a60ecc807d76. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java:608

          registrationFraudChecker.checkSendVerificationCodeAttempt(requestContext, storedVerificationSession,
                  registrationServiceSession.number())
              .updatedSession()
              .orElse(storedVerificationSession);
    }

    if (registrationServiceSession.verified()) {
      throw new ClientErrorException(
          Response.status(Response.Status.CONFLICT)
              .entity(buildResponse(registrationServiceSession, verificationSession))
              .build());
    }

    if (!verificationSession.allowedToRequestCode()) {
      final Response.Status status = verificationSession.requestedInformation().isEmpty()
          ? Response.Status.TOO_MANY_REQUESTS
          : Response.Status.CONFLICT;

      throw new ClientErrorException(
          Response.status(status)
              .entity(buildResponse(registrationServiceSession, verificationSession))
              .build());
    }

    final MessageTransport messageTransport = verificationCodeRequest.transport().toMessageTransport();

    final ClientType clientType = switch (verificationCodeRequest.client()) {
      case "ios" -> ClientType.IOS;
      case "android-2021-03" -> ClientType.ANDROID_WITH_FCM;
      default -> {
        if (Strings.CI.startsWith(verificationCodeRequest.client(), "android")) {
          yield ClientType.ANDROID_WITHOUT_FCM;
        }
        yield ClientType.UNKNOWN;
      }
    };

View on GitHub (pinned to 100ab61c82)