apereo/cas · warning

Throttled submission

Error message

Throttled submission [{}] remains throttled; submission expires at [{}]

What it means

AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter.exceedsThreshold checks the in-memory throttle store for an existing entry for the failure key. If a record exists and has not expired, the request is still considered throttled; this warning reports the key and expiration time, and the request attribute ThrottledSubmission is set before the request is rejected.

Solutions

  1. Wait until the reported expiration time passes; the in-memory store expires entries automatically.
  2. Restart CAS or clear the in-memory throttle store to immediately lift the block (dev/testing).
  3. Adjust cas.authn.throttle.failure.threshold-rate / range to a less aggressive policy.
  4. Allow-list or bypass the offending trusted client/IP if it is a legitimate automated caller.

Example fix

// before (too aggressive)
cas.authn.throttle.failure.threshold=3
cas.authn.throttle.failure.range-seconds=3600
// after
cas.authn.throttle.failure.threshold=10
cas.authn.throttle.failure.range-seconds=60
Defensive patterns

Strategy: fallback

Validate before calling

// client-side: back off until expiration before retrying
if (response.containsHeader("Retry-After")) {
    long waitSecs = Long.parseLong(response.getFirstHeader("Retry-After").getValue());
    Thread.sleep(TimeUnit.SECONDS.toMillis(waitSecs));
}

Try / catch

// treat HTTP 429 / throttled response as transient
try {
    return authenticate(credentials);
} catch (ThrottledSubmissionException e) {
    LOGGER.warn("Throttled until {}", e.getExpiration());
    return AuthenticationResult.throttled(e.getExpiration());
}

Prevention

When it happens

Trigger: A client whose identifier (username/IP) was previously recorded as a failed authentication attempt submits again before the throttle interval defined by the failure threshold rate has elapsed.

Common situations: User repeatedly failing password attempts and being locked out; an automated client (script, monitoring probe) hammering the login endpoint from one IP; stale throttle entries after clock/config changes to threshold rate.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/b159fb2e3008cfb8. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-throttle-core/src/main/java/org/apereo/cas/throttle/AbstractInMemoryThrottledSubmissionHandlerInterceptorAdapter.java:65

            .clientIpAddress(ClientInfoHolder.getClientInfo().getClientIpAddress())
            .build();
        LOGGER.debug("Recording submission failure entry [{}]", submission);
        getConfigurationContext().getThrottledSubmissionStore().put(submission);
        throttledSubmissionReceivers.forEach(Unchecked.consumer(receiver -> receiver.receive(submission)));
        LOGGER.info("Recorded submission failure [{}] for [{}]", submission, key);
    }

    @Override
    public boolean exceedsThreshold(final HttpServletRequest request) {
        val key = constructKey(request);
        LOGGER.trace("Throttling threshold key is [{}] with calculated threshold [{}]", key, getThresholdRate());
        val store = getConfigurationContext().getThrottledSubmissionStore();

        if (store.contains(key)) {
            val submission = store.get(key);
            LOGGER.trace("Found existing throttled submission [{}] for key [{}]", submission, key);
            if (!Objects.requireNonNull(submission).hasExpiredAlready()) {
                LOGGER.warn("Throttled submission [{}] remains throttled; submission expires at [{}]", key, submission.getExpiration());
                request.setAttribute(ThrottledSubmission.class.getSimpleName(), submission);
                return true;
            }
        }
        if (store.exceedsThreshold(key, getThresholdRate())) {
            val submission = store.get(key);
            request.setAttribute(ThrottledSubmission.class.getSimpleName(), submission);
            return true;
        }
        return false;
    }

    @Override
    public void release() {
        try {
            LOGGER.debug("Beginning audit cleanup...");
            getConfigurationContext().getThrottledSubmissionStore().release(getThresholdRate());
        } finally {

View on GitHub (pinned to e7288fc434)