{"record":{"id":"55cc710b5f272f3b","repo":"quarkusio/quarkus","slug":"rate-limit-exceeded-too-many-acme-challenge-reque","errorCode":null,"errorMessage":"Rate limit exceeded: too many ACME challenge requests. Wait 60 seconds and try again.","messagePattern":"Rate limit exceeded: too many ACME challenge requests\\. Wait 60 seconds and try again\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/AcmeClient.java","lineNumber":102,"sourceCode":"    }\n\n    private void checkRateLimit(String operation) {\n        long now = System.currentTimeMillis();\n        long windowStart = windowStartTime.get();\n\n        if (now - windowStart >= RATE_LIMIT_WINDOW_MS) {\n            if (windowStartTime.compareAndSet(windowStart, now)) {\n                requestCount.set(0);\n            }\n        }\n\n        int currentCount = requestCount.incrementAndGet();\n        if (currentCount > MAX_REQUESTS_PER_MINUTE) {\n            AUDIT.warn(\"Rate limit exceeded - operation: \" + operation + \", requests: \" + currentCount + \"/\"\n                    + MAX_REQUESTS_PER_MINUTE + \", endpoint: \" + challengeUrl);\n            LOGGER.warn(\"⚠️  Rate limit exceeded: \" + currentCount + \" requests in the last minute\");\n            LOGGER.warn(\"⚠️  Maximum allowed: \" + MAX_REQUESTS_PER_MINUTE + \" requests per minute\");\n            throw new RuntimeException(\n                    \"Rate limit exceeded: too many ACME challenge requests. Wait 60 seconds and try again.\");\n        }\n    }\n\n    public boolean checkReadiness() {\n\n        // Check status\n        LOGGER.infof(\"\\uD83D\\uDD35 Checking management challenge endpoint status using %s\", challengeUrl);\n        HttpRequest<Buffer> request = managementClient.getAbs(challengeUrl);\n        addKeyAndUser(request);\n        try {\n            HttpResponse<Buffer> response = await(request.send());\n            int status = response.statusCode();\n            switch (status) {\n                case 200, 204 -> {\n                    return true;\n                }\n                case 404 -> {","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/tls-registry/cli/src/main/java/io/quarkus/tls/cli/letsencrypt/AcmeClient.java#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before\nwhile (!ready) { acmeClient.certificateChainAndKeyAreReady(...); Thread.sleep(200); }\n\n// after\nlong backoff = 1000;\nwhile (!ready) {\n    try {\n        ready = acmeClient.certificateChainAndKeyAreReady(...);\n    } catch (RuntimeException e) {\n        Thread.sleep(60_000); // honor rate-limit window\n        continue;\n    }\n    Thread.sleep(backoff);\n    backoff = Math.min(backoff * 2, 30_000);\n}","handlingStrategy":"retry","validationCode":"import java.time.Instant;\n\nprivate static final int MAX_REQUESTS_PER_MINUTE = 30; // match AcmeClient limit\nprivate final java.util.ArrayDeque<Instant> recentCalls = new java.util.ArrayDeque<>();\n\nsynchronized boolean mayCallNow() {\n    Instant now = Instant.now();\n    while (!recentCalls.isEmpty() && recentCalls.peek().isBefore(now.minusSeconds(60))) {\n        recentCalls.poll();\n    }\n    if (recentCalls.size() >= MAX_REQUESTS_PER_MINUTE) return false;\n    recentCalls.add(now);\n    return true;\n}\n// call mayCallNow() before each AcmeClient operation\n","typeGuard":null,"tryCatchPattern":"try {\n    acmeClient.proveIdentifierControl(domain, account, ...);\n} catch (RuntimeException e) {\n    if (e.getMessage().contains(\"Rate limit exceeded\")) {\n        Thread.sleep(61_000); // let the 60s window reset\n        // retry once with backoff, do not loop tightly\n    } else {\n        throw e;\n    }\n}","preventionTips":["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"],"tags":["acme","letsencrypt","rate-limit","http01-challenge"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}