elastic/elasticsearch · error · IllegalArgumentException

prefetchTime [${prefetchTime}] must be greater than or equal

Error message

prefetchTime [${prefetchTime}] must be greater than or equal to staleTime [${staleTime}]

What it means

A relational guard: prefetchTime must be >= staleTime. The comment in source explains that if prefetch starts later than stale, the prefetchAt instant would fall after staleAt and the background-refresh window in resolveIdentity() would be unreachable - so the provider would always hit the synchronous fallback. The check fires only after both individual positivity checks pass.

Source

Thrown at libs/workload-identity-aws/src/main/java/org/elasticsearch/workload/identity/aws/AsyncWebIdentityCredentialsProvider.java:103

    private AsyncWebIdentityCredentialsProvider(Builder builder) {
        this.roleArn = Objects.requireNonNull(builder.roleArn, "roleArn must not be null");
        this.roleSessionName = Objects.requireNonNull(builder.roleSessionName, "roleSessionName must not be null");
        this.tokenSupplier = Objects.requireNonNull(builder.tokenSupplier, "tokenSupplier must not be null");
        this.stsAsyncClient = Objects.requireNonNull(builder.stsAsyncClient, "stsAsyncClient must not be null");
        this.prefetchTime = builder.prefetchTime != null ? builder.prefetchTime : DEFAULT_PREFETCH_TIME;
        this.staleTime = builder.staleTime != null ? builder.staleTime : DEFAULT_STALE_TIME;
        this.clock = builder.clock != null ? builder.clock : Clock.systemUTC();
        if (staleTime.isNegative() || staleTime.isZero()) {
            throw new IllegalArgumentException("staleTime must be a positive duration but was [" + staleTime + "]");
        }
        if (prefetchTime.isNegative() || prefetchTime.isZero()) {
            throw new IllegalArgumentException("prefetchTime must be a positive duration but was [" + prefetchTime + "]");
        }
        // prefetchTime must start no later than staleTime, otherwise prefetchAt would fall after staleAt and the
        // background-refresh window in resolveIdentity() would be unreachable.
        if (prefetchTime.compareTo(staleTime) < 0) {
            throw new IllegalArgumentException(
                "prefetchTime [" + prefetchTime + "] must be greater than or equal to staleTime [" + staleTime + "]"
            );
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    @Override
    public CompletableFuture<AwsCredentialsIdentity> resolveIdentity(ResolveIdentityRequest request) {
        Cached current = cache.get();
        Instant now = clock.instant();
        if (current == null || now.isAfter(current.staleAt())) {
            // Nothing usable cached: wait on a refresh, but asynchronously, so the caller's thread is not parked.
            return refresh().thenApply(Cached::credentials);
        }
        if (now.isAfter(current.prefetchAt())) {

View on GitHub (pinned to db6a809a66)

Solutions

  1. Set prefetchTime >= staleTime (e.g. prefetch = stale, or prefetch larger)
  2. If unsure, leave both null and accept the documented defaults, which already satisfy the invariant
  3. Re-read the doc comment: prefetchTime is when background refresh kicks in, staleTime is when the cached value is considered unusable - prefetch must not be later than stale

Example fix

// before
.prefetchTime(Duration.ofSeconds(10)).staleTime(Duration.ofSeconds(60)).build()
// after
.prefetchTime(Duration.ofSeconds(60)).staleTime(Duration.ofSeconds(60)).build()
Defensive patterns

Strategy: validation

Validate before calling

if (prefetchTime != null && staleTime != null
    && prefetchTime.compareTo(staleTime) < 0) {
    throw new IllegalArgumentException("prefetchTime must be >= staleTime");
}
builder.prefetchTime(prefetchTime).staleTime(staleTime);

Type guard

static boolean prefetchNotBeforeStale(Duration prefetch, Duration stale) {
    return prefetch == null || stale == null || prefetch.compareTo(stale) >= 0;
}

Try / catch

try { builder.prefetchTime(p).staleTime(s).build(); }
catch (IllegalArgumentException e) { /* align the two values */ }

Prevention

When it happens

Trigger: Building the provider with prefetchTime strictly less than staleTime (both positive). E.g. prefetchTime = 10s, staleTime = 60s.

Common situations: Treating prefetchTime as "lead time before expiry" and staleTime as "absolute window" and inverting them; copying values from a doc that defined the terms differently; auto-derived values where prefetch shrinks below stale.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/28557ae9be7b11d4. Report an issue: GitHub.