Skip to main content

RateLimiter

← all patterns

pattern

Bounds how many calls may start per time window — a fixed number of permits (limit) per fixed-length window (period) — rejecting or delaying calls once the window's permits are used up.

01

Fixed-window mechanism

  • One RateLimiter instance is one shared budget: limit calls per period. All callers compete for the same window.
  • The window is aligned to the instant the limiter was created (not to wall-clock boundaries like the top of the minute) and advances in whole periods, as measured by its Clock.
  • State is a single atomic pair — window start instant and permits used in that window — updated with compare-and-set. There is no lock; concurrent callers race for permits.
  • On every acquire attempt: if the current time has moved past the window's end, the window is advanced forward by however many whole periods have elapsed and the used count resets to zero. If the (possibly just-advanced) window still has permits left, the caller CAS-increments the used count to claim one.
  • A caller that loses the CAS race retries: it spins briefly (Thread.onSpinWait()) for the first few attempts, then falls back to Thread.yield() under sustained contention.
  • Unused permits never carry over — a new window always starts at zero used, regardless of how many permits were left unclaimed in the previous one.

02

Behavior

By default (maxWait zero) a call made while the window's permits are exhausted is rejected immediately — the operation never runs.

With withMaxWait set, an excess call instead waits for the next window: it sleeps (via the configured Clock, cheap on a virtual thread) until the next window opens, then competes for a permit there, repeating until it acquires one or its wait deadline passes. If the next window would open after the deadline, the call is rejected without sleeping that long.

A permit acquired successfully lets the operation run and its outcome is reported as usual; a rejection never executes the operation. Interruption while waiting is treated as failure: the caller's interrupt status is restored and the call fails with a ResilientException wrapping the InterruptedException, never a silent retry.

03

Extreme values

Window and deadline arithmetic is done on Instant/Duration, which can overflow with a large enough maxWait, a window left idle for a very long time, or a custom Clock returning a value near Instant.MAX. RateLimiter never lets that escape as an exception:

  • Durations are clamped before conversion to milliseconds so Duration.toMillis() cannot overflow.
  • A deadline computation that would overflow is clamped to Instant.MAX instead — an unreachable deadline simply means the wait is correctly never satisfied.
  • If the next window's start itself would overflow, the call is rejected immediately — no maxWait, however generous, could wait that out.
  • If even the elapsed-time arithmetic used to advance the window overflows, the window resets straight to the current instant instead of throwing.

04

Configuration

propertyrequireddescription
nameyesIdentifier used in events and exceptions. First positional argument of of(name, limit, period), no wither
limityesMaximum calls allowed per period. Positional argument of of(); also settable via withLimit(). Must be >= 1
periodyesLength of the fixed window. Positional argument of of(); also settable via withPeriod(). Must be positive
maxWaitnoHow long an excess call may wait for the next window before being rejected. Default: Duration.ZERO (reject immediately)
withListener()noSubscribe to rate limiter events (Permitted, Rejected)
withClock()noUse a custom Clock instead of system (mainly for testing)
name() / limit() / period() / maxWait()inspectRead back the configured values

05

Events & failure

  • Permitted — a permit was granted and the call will proceed. Carries: timestamp, name, remaining permits in the current window.
  • Rejected — no permit was available within the configured wait. Carries: timestamp, name, estimated wait until the next permit is likely available.

Throws RateLimiterException when a call is rejected because no permit became available within maxWait. Fields: name, limit, period, max wait. The name carried here must be a static, compile-time-known string, since it is used as a cardinality-bounded key in metrics and logging.