elastic/elasticsearch · error · IllegalArgumentException

staleTime must be a positive duration but was [${staleTime}]

Error message

staleTime must be a positive duration but was [${staleTime}]

What it means

The AsyncWebIdentityCredentialsProvider builder rejects a staleTime Duration that is zero or negative. staleTime defines how long before credential expiry a cached entry is considered stale and triggers refresh; a non-positive value would never allow staleness. The check runs in the private constructor after defaults are applied.

Source

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

    private final Consumer<ActionListener<String>> tokenSupplier;
    private final StsAsyncClient stsAsyncClient;
    private final Duration prefetchTime;
    private final Duration staleTime;
    private final Clock clock;

    private final AtomicReference<Cached> cache = new AtomicReference<>();
    private final AtomicReference<CompletableFuture<Cached>> inFlight = new AtomicReference<>();

    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

View on GitHub (pinned to db6a809a66)

Solutions

  1. Set staleTime to a positive Duration (e.g. Duration.ofMinutes(5))
  2. If you want aggressive refresh, use a small positive value, not zero
  3. Validate config-sourced durations before passing to the builder

Example fix

// before
.provider.staleTime(Duration.ZERO).build()
// after
.provider.staleTime(Duration.ofSeconds(30)).build()
Defensive patterns

Strategy: validation

Validate before calling

if (staleTime == null || staleTime.isZero() || staleTime.isNegative()) {
    throw new IllegalArgumentException("staleTime must be positive: " + staleTime);
}
builder.staleTime(staleTime);

Type guard

static boolean isPositiveDuration(Duration d) {
    return d != null && !d.isZero() && !d.isNegative();
}

Try / catch

try { builder.staleTime(d).build(); }
catch (IllegalArgumentException e) { /* use default */ }

Prevention

When it happens

Trigger: Building the provider with .staleTime(Duration.ofSeconds(0)) or a negative Duration. The default (DEFAULT_STALE_TIME) is used when null, so this only fires when an explicit non-positive Duration is supplied.

Common situations: Computing staleTime from a config knob typed as 0; passing Duration.ZERO meaning "refresh immediately"; mis-tuning refresh windows; arithmetic that produces a negative duration when expiry is near.

Related errors


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