RateLimiter
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
RateLimiterinstance is one shared budget:limitcalls perperiod. 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 toThread.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.MAXinstead — 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
| property | required | description |
|---|---|---|
| name | yes | Identifier used in events and exceptions. First positional argument of of(name, limit, period), no wither |
| limit | yes | Maximum calls allowed per period. Positional argument of of(); also settable via withLimit(). Must be >= 1 |
| period | yes | Length of the fixed window. Positional argument of of(); also settable via withPeriod(). Must be positive |
| maxWait | no | How long an excess call may wait for the next window before being rejected. Default: Duration.ZERO (reject immediately) |
| withListener() | no | Subscribe to rate limiter events (Permitted, Rejected) |
| withClock() | no | Use a custom Clock instead of system (mainly for testing) |
| name() / limit() / period() / maxWait() | inspect | Read 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.