signalapp/Signal-Server · warning · RateLimitExceededException

Encountered a negative retry duration

Error message

Encountered a negative retry duration: {}, will not include a Retry-After header in response

What it means

The RateLimitExceededException mapper builds a 429 response and includes a Retry-After header from the exception's retry duration. If that duration is negative (a caller constructed the exception with a bad/already-elapsed duration), the mapper logs a warning and simply omits the Retry-After header; the 429 is still returned.

Solutions

  1. Fix the rate limiter code to compute retry duration as remaining-budget divided by refill rate, clamped with Duration.ofSeconds(Math.max(1, ...)) or similar.
  2. Check for clock skew on the host if durations derive from wall-clock timestamps.
  3. Treat the warning as a bug indicator: Retry-After is being silently dropped for those 429s.
  4. Write a unit test asserting non-negative retry durations from every RateLimitExceededException construction site.

Example fix

// before
Duration retryDuration = Duration.between(Instant.now(), lastRefill); // negative if refilled
throw new RateLimitExceededException(retryDuration);

// after
Duration retryDuration = Duration.between(Instant.now(), nextRefillInstant);
throw new RateLimitExceededException(retryDuration.isNegative() ? Duration.ofSeconds(1) : retryDuration);
Defensive patterns

Strategy: validation

Validate before calling

// before constructing the exception
Duration d = Duration.between(Instant.now(), nextRefillInstant);
if (d.isNegative()) { d = Duration.ZERO; } // or log and clamp
new RateLimitExceededException(d);

Prevention

When it happens

Trigger: Throwing RateLimitExceededException with a retry duration computed from a timestamp already in the past (e.g. clock skew, or Duration.between arguments inverted), yielding a negative Optional duration.

Common situations: Rate limiter implementations computing 'next refill minus now' with clock drift, or constructing the exception before computing the duration correctly.

Related errors


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

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/mappers/RateLimitExceededExceptionMapper.java:31

@Provider
public class RateLimitExceededExceptionMapper implements ExceptionMapper<RateLimitExceededException> {

  private static final Logger logger = LoggerFactory.getLogger(RateLimitExceededExceptionMapper.class);

  /**
   * Convert a RateLimitExceededException to a 429 response
   * with a Retry-After header.
   *
   * @param e A RateLimitExceededException potentially containing a recommended retry duration
   * @return the response
   */
  @Override
  public Response toResponse(RateLimitExceededException e) {
    return e.getRetryDuration()
        .filter(d -> {
          if (d.isNegative()) {
            logger.warn("Encountered a negative retry duration: {}, will not include a Retry-After header in response",
                d);
          }
          // only include non-negative durations in retry headers
          return !d.isNegative();
        })
        .map(d -> Response.status(Response.Status.TOO_MANY_REQUESTS).header("Retry-After", d.toSeconds()))
        .orElseGet(() -> Response.status(Response.Status.TOO_MANY_REQUESTS)).build();
  }
}

View on GitHub (pinned to 100ab61c82)