quarkusio/quarkus · error · RuntimeException
Rate limit exceeded: too many ACME challenge requests. Wait
Error message
Rate limit exceeded: too many ACME challenge requests. Wait 60 seconds and try again.
What it means
AcmeClient implements a client-side rate limiter: checkRateLimit increments a per-minute counter and, once more than MAX_REQUESTS_PER_MINUTE ACME challenge requests are issued within a rolling minute, it throws this RuntimeException. It protects the local ACME challenge/management endpoints (and the Let's Encrypt workflow) from request floods, e.g. retry loops that poll or re-upload challenges too aggressively.
Source
Thrown at extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/AcmeClient.java:102
}
private void checkRateLimit(String operation) {
long now = System.currentTimeMillis();
long windowStart = windowStartTime.get();
if (now - windowStart >= RATE_LIMIT_WINDOW_MS) {
if (windowStartTime.compareAndSet(windowStart, now)) {
requestCount.set(0);
}
}
int currentCount = requestCount.incrementAndGet();
if (currentCount > MAX_REQUESTS_PER_MINUTE) {
AUDIT.warn("Rate limit exceeded - operation: " + operation + ", requests: " + currentCount + "/"
+ MAX_REQUESTS_PER_MINUTE + ", endpoint: " + challengeUrl);
LOGGER.warn("⚠️ Rate limit exceeded: " + currentCount + " requests in the last minute");
LOGGER.warn("⚠️ Maximum allowed: " + MAX_REQUESTS_PER_MINUTE + " requests per minute");
throw new RuntimeException(
"Rate limit exceeded: too many ACME challenge requests. Wait 60 seconds and try again.");
}
}
public boolean checkReadiness() {
// Check status
LOGGER.infof("\uD83D\uDD35 Checking management challenge endpoint status using %s", challengeUrl);
HttpRequest<Buffer> request = managementClient.getAbs(challengeUrl);
addKeyAndUser(request);
try {
HttpResponse<Buffer> response = await(request.send());
int status = response.statusCode();
switch (status) {
case 200, 204 -> {
return true;
}
case 404 -> {View on GitHub (pinned to e1c734241f)
Solutions
- Wait 60 seconds for the request counter window to reset, then retry the operation
- Add exponential backoff to your retry logic around AcmeClient calls instead of fixed short delays
- Reduce polling frequency: check certificateChainAndKeyAreReady readiness with longer intervals between checks
- Deduplicate concurrent certificate requests (a single scheduled renewal job per account instead of per-instance loops)
Example fix
// before
while (!ready) { acmeClient.certificateChainAndKeyAreReady(...); Thread.sleep(200); }
// after
long backoff = 1000;
while (!ready) {
try {
ready = acmeClient.certificateChainAndKeyAreReady(...);
} catch (RuntimeException e) {
Thread.sleep(60_000); // honor rate-limit window
continue;
}
Thread.sleep(backoff);
backoff = Math.min(backoff * 2, 30_000);
} Defensive patterns
Strategy: retry
Validate before calling
import java.time.Instant;
private static final int MAX_REQUESTS_PER_MINUTE = 30; // match AcmeClient limit
private final java.util.ArrayDeque<Instant> recentCalls = new java.util.ArrayDeque<>();
synchronized boolean mayCallNow() {
Instant now = Instant.now();
while (!recentCalls.isEmpty() && recentCalls.peek().isBefore(now.minusSeconds(60))) {
recentCalls.poll();
}
if (recentCalls.size() >= MAX_REQUESTS_PER_MINUTE) return false;
recentCalls.add(now);
return true;
}
// call mayCallNow() before each AcmeClient operation
Try / catch
try {
acmeClient.proveIdentifierControl(domain, account, ...);
} catch (RuntimeException e) {
if (e.getMessage().contains("Rate limit exceeded")) {
Thread.sleep(61_000); // let the 60s window reset
// retry once with backoff, do not loop tightly
} else {
throw e;
}
} Prevention
- Add exponential backoff to any retry loop around ACME challenge operations
- Poll certificate readiness (checkReadiness / certificateChainAndKeyAreReady) at intervals of several seconds, not sub-second
- Run a single certificate renewal scheduler per account, not one per application instance
- Track the client-side counter yourself before calling and skip the call when the window is exhausted
When it happens
Trigger: Calling proveIdentifierControl, cleanupAfterChallenge, or certificateChainAndKeyAreReady more than MAX_REQUESTS_PER_MINUTE times within 60 seconds — typically an application-level retry loop that repeatedly uploads the HTTP-01 challenge or polls readiness without backoff, or several certificate requests running concurrently through the same client.
Common situations: Retry frameworks (e.g. @Retryable, resilience4j) wrapping AcmeClient calls with short intervals; automated tests re-running the ACME flow in quick succession; a misconfigured scheduler renewing certificates in a tight loop; multiple application instances sharing one ACME account hitting the same challenge endpoints.
Related errors
- Missing certificate authority challenge
- Failed to respond to certificate authority challenge
- Invalid certificate authority challenge
- Failure to save the account
- Failed to obtain certificate: <reason>
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/55cc710b5f272f3b.
Report an issue: GitHub.