@@ -37,9 +37,10 @@ idle for a configurable duration.
3737- ** Type-safe & generic** — ` Storage[K, V] ` and ` BucketLimiter[K] ` use Go
3838 generics (Go 1.26+).
3939- ** Extensible** — implement the ` Storage ` interface for a custom in-process
40- store, or the ` Limiter ` interface for a custom algorithm.
40+ store, or the ` Limiter ` interface for a custom algorithm (optionally adding
41+ ` Reserver ` for accurate ` Retry-After ` , even with a Redis/Valkey backend).
4142- ** HTTP middleware example** — with accurate ` Retry-After ` and ` RateLimit-* `
42- response headers.
43+ response headers, driven by the backend-agnostic ` Reserver ` interface .
4344
4445## Architecture
4546
@@ -155,10 +156,12 @@ func main() {
155156| Type / func | Role |
156157| ------------------------------------| ----------------------------------------------------------------------------|
157158| ` Limiter ` | Minimal interface (` Allow ` , ` Wait ` , ` Burst ` ). ` *rate.Limiter ` satisfies it. |
159+ | ` Reserver ` / ` Reservation ` | Optional capability: reserve a token and read its delay. Enables accurate ` Retry-After ` for any backend. |
160+ | ` RateLimiter ` | Default limiter: wraps ` *rate.Limiter ` , implements ` Limiter ` ** and** ` Reserver ` . |
158161| ` Storage[K, V] ` | Pluggable, concurrency-safe store for per-key limiters. |
159162| ` InMemoryStorage[K, V] ` | Default ` sync.Map ` -backed store. |
160163| ` BucketLimiter[K] ` | Manager: hands out one ` Limiter ` per key, handles creation and eviction. |
161- | ` NewRateLimiterFunc(limit, burst) ` | Convenience factory for the common ` *rate.Limiter ` case. |
164+ | ` NewRateLimiterFunc(limit, burst) ` | Convenience factory producing ` RateLimiter ` values. |
162165
163166### ` limit ` and ` burst `
164167
@@ -186,6 +189,33 @@ if err := lim.Wait(ctx); err != nil {
186189}
187190```
188191
192+ ### Reserve (for accurate ` Retry-After ` )
193+
194+ When you need the exact delay until the next token — to set a ` Retry-After ` or
195+ ` RateLimit-Reset ` header — use the optional ` Reserver ` capability. Feature-detect
196+ it so your code works with any limiter and degrades gracefully:
197+
198+ ``` go
199+ lim := manager.GetOrAdd (key)
200+
201+ if r , ok := lim.(ratelimiter.Reserver ); ok {
202+ res := r.Reserve ()
203+ if res.OK () && res.Delay () == 0 {
204+ // proceed now
205+ } else {
206+ res.Cancel () // return the token
207+ retryAfter := res.Delay () // tell the client exactly how long to wait
208+ }
209+ } else {
210+ _ = lim.Allow () // limiter without reservation support: no timing info
211+ }
212+ ```
213+
214+ The default ` RateLimiter ` from ` NewRateLimiterFunc ` implements ` Reserver ` , and a
215+ custom (e.g. Redis/Valkey-backed) ` Limiter ` can too — so the same middleware
216+ produces accurate headers regardless of backend. See
217+ [ docs/CUSTOM_STORAGE.md] ( docs/CUSTOM_STORAGE.md#distributed-limiting-with-redis--valkey ) .
218+
189219## HTTP middleware
190220
191221The [ ` examples/middleware ` ] ( examples/middleware/main.go ) program limits requests
@@ -211,10 +241,12 @@ go run ./examples/key -limit 1 -burst 3
211241
212242## Custom storage
213243
214- Implement ` Storage[K, V] ` to back the manager with your own in-process store —
215- for example a size-bounded LRU to cap memory instead of (or in addition to)
216- time-based eviction. Implementations must be safe for concurrent use, and
217- ` LoadOrStore ` must be atomic.
244+ ` BucketLimiter ` talks to the ` Storage[K, V] ` interface, never a concrete map,
245+ and you inject the implementation at construction time. ` InMemoryStorage ` is
246+ just the bundled ** default** — implement the interface to bring your own
247+ in-process store (for example a size-bounded LRU to cap memory instead of, or in
248+ addition to, time-based eviction). Implementations must be safe for concurrent
249+ use, and ` LoadOrStore ` must be atomic.
218250
219251``` go
220252type Storage [K comparable, V any] interface {
@@ -226,6 +258,19 @@ type Storage[K comparable, V any] interface {
226258}
227259```
228260
261+ A complete, runnable size-bounded LRU store lives in
262+ [ ` examples/customstorage ` ] ( examples/customstorage/main.go ) :
263+
264+ ``` bash
265+ go run ./examples/customstorage -cap 2
266+ ```
267+
268+ ** [ docs/CUSTOM_STORAGE.md] ( docs/CUSTOM_STORAGE.md ) ** is a full guide: the method
269+ contracts, how to test atomicity, and — importantly — why a custom ` Storage ` is
270+ ** in-process only** , plus the correct pattern for ** distributed limiting with
271+ Redis / [ Valkey] ( https://github.com/valkey-io/valkey-go ) ** (a datastore-backed
272+ ` Limiter ` wired through a ` Storage ` resolver, with the token-bucket Lua script).
273+
229274## Scope: single-process only
230275
231276Token state lives in memory inside each ` *rate.Limiter ` , so this library
0 commit comments