ben-manes/caffeine · error · IllegalArgumentException

key %s invalid format; was %s, must end with one of [dDhHmMs

Error message

key %s invalid format; was %s, must end with one of [dDhHmMsS]

What it means

CaffeineSpec.parseTimeUnit inspects the last character of a simple-format duration and throws IllegalArgumentException("key %s invalid format; was %s, must end with one of [dDhHmMsS]") unless it is d/h/m/s (case-insensitive). Only day, hour, minute, and second units are supported in the simple format; sub-second durations require ISO-8601.

Source

Thrown at caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java:350

  }

  /** Returns a parsed {@link TimeUnit} value. */
  @SuppressWarnings({"ConstantValue", "StatementSwitchToExpressionSwitch"})
  static TimeUnit parseTimeUnit(String key, String value) {
    requireArgument((value != null) && !value.isEmpty(), "value of key %s omitted", key);
    @SuppressWarnings("null")
    char lastChar = Character.toLowerCase(value.charAt(value.length() - 1));
    switch (lastChar) {
      case 'd':
        return TimeUnit.DAYS;
      case 'h':
        return TimeUnit.HOURS;
      case 'm':
        return TimeUnit.MINUTES;
      case 's':
        return TimeUnit.SECONDS;
      default:
        throw new IllegalArgumentException(String.format(US,
            "key %s invalid format; was %s, must end with one of [dDhHmMsS]", key, value));
    }
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (this == o) {
      return true;
    } else if (!(o instanceof CaffeineSpec)) {
      return false;
    }
    var spec = (CaffeineSpec) o;
    return Objects.equals(refreshAfterWrite, spec.refreshAfterWrite)
        && Objects.equals(expireAfterAccess, spec.expireAfterAccess)
        && Objects.equals(expireAfterWrite, spec.expireAfterWrite)
        && Objects.equals(initialCapacity, spec.initialCapacity)
        && Objects.equals(maximumWeight, spec.maximumWeight)
        && Objects.equals(maximumSize, spec.maximumSize)

View on GitHub (pinned to 9da6581ee3)

Solutions

  1. Express the duration with an allowed unit: d/h/m/s, e.g. expireAfterWrite=30s, 10m, 2h, 1d
  2. For sub-second durations use ISO-8601: expireAfterWrite=PT0.5S
  3. When converting from millis, compute the best whole unit programmatically before building the spec

Example fix

# before
Caffeine.from("expireAfterWrite=500ms") # IllegalArgumentException: must end with [dDhHmMsS]

# after
Caffeine.from("expireAfterWrite=PT0.5S") # 500ms via ISO-8601
# or
Caffeine.from("expireAfterWrite=1s")
Defensive patterns

Strategy: validation

Validate before calling

// Validate simple duration format before parsing:
static boolean validSimpleDuration(String v) {
  return v != null && v.length() >= 2 && "dDhHmMsS".indexOf(v.charAt(v.length() - 1)) >= 0
      && v.substring(0, v.length() - 1).matches("[+-]?\\d+([_]\\d+)*");
}
if (!validSimpleDuration(value)) {
  // convert millis to a supported unit or ISO-8601
  spec = "expireAfterWrite=" + Duration.ofMillis(millis)); // PT0.5S style
}

Try / catch

try {
  return Caffeine.from(spec);
} catch (IllegalArgumentException e) {
  if (e.getMessage() != null && e.getMessage().contains("[dDhHmMsS]")) {
    throw new ConfigException("Duration must use d/h/m/s suffix or ISO-8601: " + spec, e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Caffeine.from("expireAfterWrite=500ms") (milliseconds not supported in simple format); a bare number "expireAfterWrite=30"; a typo'd suffix like "30min" or "1hr"; an empty value after key stripping.

Common situations: Porting configs from libraries that accept ms/ns units; expecting millis because other tools use them; forgetting that ISO-8601 (PT0.5S) is needed for sub-second expiry.

Related errors


AI-assisted analysis of ben-manes/caffeine@9da6581ee3 (2026-08-14). Data as JSON: /api/errors/bb51e420d133d475. Report an issue: GitHub.