rate-net 0.9.0

A powerful, lock-free rate limiter for Rust: multiple algorithms behind one trait, sharded per-key state, bounded-memory eviction, retry-after, and a one-line Tier-1 API. Built against hostile traffic.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
<h1 align="center">
    <img width="99" alt="Rust logo" src="https://raw.githubusercontent.com/jamesgober/rust-collection/72baabd71f00e14aa9184efcb16fa3deddda3a0a/assets/rust-logo.svg">
    <br><b>rate-net</b><br>
    <sub><sup>API REFERENCE</sup></sub>
</h1>
<div align="center">
    <sup>
        <a href="../README.md" title="Project Home"><b>HOME</b></a>
        <span>&nbsp;&nbsp;</span>
        <span>API</span>
        <span>&nbsp;&nbsp;</span>
        <a href="../CHANGELOG.md" title="Changelog"><b>CHANGELOG</b></a>
    </sup>
</div>
<br>

> Complete reference for every public item in `rate-net`, with examples. The
> format mirrors the portfolio standard
> ([metrics-lib API.md]https://github.com/jamesgober/metrics-lib/blob/main/docs/API.md).
>
> **Status: pre-1.0 (`v0.9.0`, beta) — API frozen.** The public surface
> documented here will not change before `1.0` except for additive,
> backward-compatible items. Every public type is asserted `Send + Sync +
> 'static` at compile time, and every algorithm is stress-tested under
> contention.
> Five algorithms behind one [`Limiter`]#limiter-trait trait, the Tier-2
> [`Builder`]#builder, an optional [`AsyncLimiter`]#asynclimiter
> await-until-ready layer, runnable
> [examples]https://github.com/jamesgober/rate-net/tree/main/examples, and a
> [benchmark suite]./BENCHMARKS.md (with an honest head-to-head vs `governor`) —
> over a sharded, bounded-memory, allocation-free core hardened against
> adversarial traffic. The leaky bucket and window algorithms require the
> `algorithms` feature; `AsyncLimiter` requires `async`.

## Table of Contents

- [Overview]#overview
- [Public API]#public-api
  - [`RateLimiter`]#ratelimiter
    - [`per_second`]#ratelimiterper_second
    - [`per_minute`]#ratelimiterper_minute
    - [`with_quota`]#ratelimiterwith_quota
    - [`with_clock`]#ratelimiterwith_clock
    - [`with_shards`]#ratelimiterwith_shards
    - [`with_eviction`]#ratelimiterwith_eviction
    - [`with_algorithm`]#ratelimiterwith_algorithm
    - [`check`]#ratelimitercheck
    - [`check_n`]#ratelimitercheck_n
    - [`quota` / `algorithm` / `shards` / `eviction` / `tracked_keys`]#ratelimiter-introspection
  - [`Builder`]#builder
  - [`AsyncLimiter`]#asynclimiter _(feature: `async`)_
  - [`Limiter` trait]#limiter-trait
  - [`Decision`]#decision
  - [`Quota`]#quota
  - [`Eviction`]#eviction
  - [`Algorithm`]#algorithm
  - [`RateLimiterError`]#ratelimitererror
  - [`Key`]#key
  - [`VERSION`]#version
- [Sharding and eviction]#sharding-and-eviction
- [Tier 2 — the configured path]#tier-2--the-configured-path
- [Algorithms]#algorithms
- [Feature flags]#feature-flags

---

## Overview

`rate-net` answers "is this key allowed right now?" with a [`Decision`](#decision)
(`Allow` / `Deny { retry_after }`), tracking an independent allowance per key.
The common case is a constructor plus `check`:

```rust
use rate_net::{RateLimiter, Decision};

let limiter = RateLimiter::per_second(100);
match limiter.check("user:42") {
    Decision::Allow => { /* serve */ }
    Decision::Deny { retry_after } => { let _ = retry_after; /* 429 + Retry-After */ }
    _ => {}
}
```

The core guarantee: **for any key and window, admitted requests never exceed the
configured quota.** The token-bucket accounting is delegated to
[`better-bucket`](https://crates.io/crates/better-bucket); time is read from an
injectable [`clock-lib`](https://crates.io/crates/clock-lib) clock.

---

## Public API

### `RateLimiter`

```rust
pub struct RateLimiter<C: Clock + Clone = SystemClock> { /* private */ }
```

A keyed rate limiter. It tracks a separate allowance for every key it sees and
answers `check` in the time it takes to hash the key and run its bucket. It is
`Send + Sync` and is meant to be shared — behind an `Arc`, or as a `static` —
across every thread serving requests; `check` takes `&self`, so no `&mut` or
external lock is needed.

The type parameter `C` is the clock source. It defaults to `SystemClock` (the OS
monotonic clock); tests inject a `ManualClock` via [`with_clock`](#ratelimiterwith_clock).
Its `Debug` impl deliberately prints only the quota, algorithm, and live key
count — never the keys themselves, which can be caller identities.

#### `RateLimiter::per_second`

```rust
pub fn per_second(limit: u32) -> RateLimiter<SystemClock>
```

A limiter allowing `limit` requests per second, per key. The headline Tier-1
constructor.

- `limit` — requests admitted per second per key. `0` yields a limiter that
  denies every request (use [`Quota::rate`]#quota when you want `0` rejected
  as an error).

```rust
use rate_net::RateLimiter;

let limiter = RateLimiter::per_second(100);
assert_eq!(limiter.quota().limit(), 100);
```

#### `RateLimiter::per_minute`

```rust
pub fn per_minute(limit: u32) -> RateLimiter<SystemClock>
```

A limiter allowing `limit` requests per minute, per key. Same zero-limit
semantics as `per_second`.

```rust
use rate_net::RateLimiter;
use std::time::Duration;

let limiter = RateLimiter::per_minute(600);
assert_eq!(limiter.quota().period(), Duration::from_secs(60));
```

#### `RateLimiter::with_quota`

```rust
pub fn with_quota(quota: Quota) -> RateLimiter<SystemClock>
```

A limiter built from an explicit [`Quota`](#quota). Pair it with
[`Quota::rate`](#quota) when the window is neither a second nor a minute.

- `quota` — the per-key rate the limiter enforces.

```rust
use rate_net::{RateLimiter, Quota};
use std::time::Duration;

// 5 requests per 100ms, per key.
let quota = Quota::rate(5, Duration::from_millis(100))?;
let limiter = RateLimiter::with_quota(quota);
assert_eq!(limiter.quota().limit(), 5);
# Ok::<(), rate_net::RateLimiterError>(())
```

#### `RateLimiter::with_clock`

```rust
pub fn with_clock<C2: Clock + Clone>(self, clock: C2) -> RateLimiter<C2>
```

Replaces the limiter's time source, discarding any per-key state. The
clock-injection seam — inject a `ManualClock` (wrapped in `Arc`) to drive refill
deterministically with no `sleep`.

- `clock` — the new time source. Any `clock_lib::Clock` that is also `Clone`;
  `Arc<ManualClock>` and `SystemClock` both qualify.

```rust
use rate_net::RateLimiter;
use clock_lib::ManualClock;
use std::sync::Arc;
use std::time::Duration;

let clock = Arc::new(ManualClock::new());
let limiter = RateLimiter::per_second(5).with_clock(Arc::clone(&clock));

for _ in 0..5 {
    assert!(limiter.check("k").is_allow());
}
assert!(limiter.check("k").is_deny());

clock.advance(Duration::from_secs(1)); // no real sleep
assert!(limiter.check("k").is_allow());
```

#### `RateLimiter::with_shards`

```rust
pub fn with_shards(self, shards: usize) -> Self
```

Sets the shard count, discarding any per-key state. Intended immediately after
construction. More shards reduce contention between unrelated keys; the value is
rounded up to a power of two. See [Sharding and eviction](#sharding-and-eviction).

- `shards` — the desired shard count (rounded up to a power of two). A small
  multiple of the core count is a good starting point.

```rust
use rate_net::RateLimiter;

let limiter = RateLimiter::per_second(1000).with_shards(64);
assert_eq!(limiter.shards(), 64);
```

#### `RateLimiter::with_eviction`

```rust
pub fn with_eviction(self, eviction: Eviction) -> Self
```

Sets the [eviction policy](#eviction), discarding any per-key state. Intended
immediately after construction. The default bounds memory with a generous
capacity cap; override it to tune the cap or add an idle TTL.

- `eviction` — the [`Eviction`]#eviction policy.

```rust
use rate_net::{RateLimiter, Eviction};
use std::time::Duration;

let limiter = RateLimiter::per_second(1000)
    .with_eviction(Eviction::capacity(100_000).with_idle(Duration::from_secs(300)));
assert_eq!(limiter.eviction().max_keys(), Some(100_000));
```

#### `RateLimiter::with_algorithm`

```rust
pub fn with_algorithm(self, algorithm: Algorithm) -> Self
```

Selects the [`Algorithm`](#algorithm), discarding any per-key state. Intended
immediately after construction. The leaky bucket and window algorithms require
the `algorithms` feature; without it the only selectable variant is
`Algorithm::TokenBucket`.

- `algorithm` — the strategy to apply.

```rust
# #[cfg(feature = "algorithms")] {
use rate_net::{RateLimiter, Algorithm};

let limiter = RateLimiter::per_second(100).with_algorithm(Algorithm::FixedWindow);
assert_eq!(limiter.algorithm(), Algorithm::FixedWindow);
# }
```

#### `RateLimiter::check`

```rust
pub fn check(&self, key: impl Into<Key>) -> Decision
```

Checks a single unit against `key`. Returns [`Decision::Allow`](#decision) if the
key is within its limit (the unit is counted), or `Decision::Deny` carrying the
wait until it would be admitted.

- `key` — anything convertible into a [`Key`]#key: a `&str`, `String`,
  byte slice, `u64`, or `IpAddr`.

```rust
use rate_net::{RateLimiter, Decision};

let limiter = RateLimiter::per_second(1);
assert_eq!(limiter.check("user:42"), Decision::Allow);
assert!(limiter.check("user:42").is_deny()); // limit reached
```

Per-IP limiting reads naturally:

```rust
use rate_net::RateLimiter;
use std::net::{IpAddr, Ipv4Addr};

let limiter = RateLimiter::per_second(20);
let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7));
let _ = limiter.check(ip);
```

#### `RateLimiter::check_n`

```rust
pub fn check_n(&self, key: impl Into<Key>, n: u32) -> Decision
```

Checks `n` units against `key` in one operation, for requests that cost more
than one unit (a batch, a weighted endpoint). All `n` units are admitted or none
are.

- `key` — the key, as for [`check`]#ratelimitercheck.
- `n` — units to take. `0` always succeeds; `n` greater than the quota can never
  succeed, and the denial's `retry_after` is `Duration::MAX`.

```rust
use rate_net::{RateLimiter, Decision};

let limiter = RateLimiter::per_second(10);
assert_eq!(limiter.check_n("tenant:acme", 4), Decision::Allow);
assert_eq!(limiter.check_n("tenant:acme", 6), Decision::Allow);
assert!(limiter.check_n("tenant:acme", 1).is_deny()); // 10 spent
```

A request larger than the quota is permanently refused:

```rust
use rate_net::{RateLimiter, Decision};
use std::time::Duration;

let limiter = RateLimiter::per_second(5);
assert_eq!(
    limiter.check_n("k", 6),
    Decision::Deny { retry_after: Duration::MAX },
);
```

<h4 id="ratelimiter-introspection">Introspection: <code>quota</code> / <code>algorithm</code> / <code>shards</code> / <code>eviction</code> / <code>tracked_keys</code></h4>

```rust
pub fn quota(&self) -> Quota
pub const fn algorithm(&self) -> Algorithm
pub fn shards(&self) -> usize
pub const fn eviction(&self) -> Eviction
pub fn tracked_keys(&self) -> usize
```

- `quota` — the [`Quota`]#quota every key is limited to.
- `algorithm` — the [`Algorithm`]#algorithm in force (currently always
  `TokenBucket`).
- `shards` — the number of shards the per-key store is split across (a power of
  two).
- `eviction` — the [`Eviction`]#eviction policy bounding the store.
- `tracked_keys` — the number of keys with live state; a momentary, advisory
  snapshot bounded by the eviction policy.

```rust
use rate_net::{RateLimiter, Algorithm};

let limiter = RateLimiter::per_second(50).with_shards(16);
assert_eq!(limiter.quota().limit(), 50);
assert_eq!(limiter.algorithm(), Algorithm::TokenBucket);
assert_eq!(limiter.shards(), 16);
assert_eq!(limiter.tracked_keys(), 0);
let _ = limiter.check("a");
assert_eq!(limiter.tracked_keys(), 1);
```

---

### `Builder`

```rust
pub struct Builder<C: Clock + Clone = SystemClock> { /* private */ }
```

The Tier-2 path, started with [`RateLimiter::builder`](#ratelimiter). Chain the
knobs you care about, then `build()`. Anything left unset keeps a sane default
(token bucket, default sharding, bounded-memory eviction); the quota defaults to
a limit of `0`, which denies everything, so set it. `build()` is infallible.

Methods: `algorithm(Algorithm)`, `quota(limit, period)`, `per_second(limit)`,
`per_minute(limit)`, `burst(u32)`, `shards(usize)`, `eviction(Eviction)`,
`clock(C2)`, and `build() -> RateLimiter<C>`.

```rust
use rate_net::{RateLimiter, Algorithm, Eviction};
use std::time::Duration;

# #[cfg(feature = "algorithms")]
let limiter = RateLimiter::builder()
    .algorithm(Algorithm::SlidingWindowCounter)
    .quota(1000, Duration::from_secs(60)) // 1000 / minute
    .burst(50)                            // allow short bursts
    .shards(64)                           // tune for core count
    .eviction(Eviction::idle(Duration::from_secs(300)))
    .build();
```

The same knobs are also available as chainable adjusters on a constructed
limiter — [`with_shards`](#ratelimiterwith_shards),
[`with_eviction`](#ratelimiterwith_eviction),
[`with_algorithm`](#ratelimiterwith_algorithm),
[`with_clock`](#ratelimiterwith_clock) — for when the builder is more than you
need.

---

### `AsyncLimiter`

_Requires the `async` feature._

```rust
pub struct AsyncLimiter<C: Clock + Clone = SystemClock> { /* private */ }
```

An await-until-ready wrapper around a [`RateLimiter`](#ratelimiter). The core is
sync and runtime-free; this optional layer adds the one thing that needs a
runtime — *waiting* for a key to become allowed.

- `new(RateLimiter<C>) -> AsyncLimiter<C>` (also `From<RateLimiter<C>>`);
  `inner() -> &RateLimiter<C>`, `into_inner() -> RateLimiter<C>`.
- `check(key) -> Decision`, `check_n(key, n) -> Decision` — synchronous
  pass-throughs.
- `async until_ready(key)`, `async until_ready_n(key, n)` — retry on each
  denial, sleeping for the reported `retry_after` (via `tokio::time::sleep`),
  until the key is admitted. Returns immediately if the request can never
  succeed (a `retry_after` of `Duration::MAX`), so it never waits forever.

`until_ready` needs a clock that actually advances (the default `SystemClock`);
under a frozen `ManualClock` the allowance never refills, so it would wait
indefinitely.

```rust
use rate_net::{AsyncLimiter, RateLimiter};

# async fn demo() {
let limiter = AsyncLimiter::new(RateLimiter::per_second(100));

// Non-blocking, same as the sync API.
let _ = limiter.check("user:42");

// Or await until the key is within its limit.
limiter.until_ready("user:42").await;
# }
```

---

### `Limiter` trait

```rust
pub trait Limiter {
    fn check_n(&self, key: impl Into<Key>, n: u32) -> Decision;
    fn check(&self, key: impl Into<Key>) -> Decision { /* default: check_n(key, 1) */ }
}
```

The shared rate-limiting surface every algorithm implements, so generic code can
hold any limiter and call `check` without naming the concrete type or its clock.
[`RateLimiter`](#ratelimiter) implements it. Implementors provide only `check_n`;
`check` defaults to one unit.

```rust
use rate_net::{Limiter, RateLimiter};

fn admit_one<L: Limiter>(limiter: &L, key: &str) -> bool {
    limiter.check(key).is_allow()
}

let limiter = RateLimiter::per_second(2);
assert!(admit_one(&limiter, "user:1"));
```

---

### `Decision`

```rust
#[non_exhaustive]
pub enum Decision {
    Allow,
    Deny { retry_after: Duration },
}
```

The outcome of a check. A check is infallible — only an allow/deny outcome, so
this is a plain enum, not a `Result`. A denial carries `retry_after`: the minimum
wait until the same request would be admitted (`Duration::MAX` if it can never
succeed). `#[non_exhaustive]`, so a `match` needs a wildcard arm.

Helper methods:

- `is_allow(&self) -> bool`
- `is_deny(&self) -> bool`
- `retry_after(&self) -> Option<Duration>` — the wait, or `None` if allowed.

```rust
use rate_net::Decision;
use std::time::Duration;

let denied = Decision::Deny { retry_after: Duration::from_millis(250) };
assert!(denied.is_deny());
assert_eq!(denied.retry_after(), Some(Duration::from_millis(250)));
assert_eq!(Decision::Allow.retry_after(), None);
```

Mapping a denial onto an HTTP `Retry-After` header:

```rust
use rate_net::{RateLimiter, Decision};

let limiter = RateLimiter::per_second(1);
let _ = limiter.check("u"); // spend the allowance
if let Decision::Deny { retry_after } = limiter.check("u") {
    let header = retry_after.as_secs().max(1); // Retry-After is whole seconds
    assert!(header >= 1);
}
```

---

### `Quota`

```rust
pub struct Quota { /* private */ }
```

A rate limit: `limit` requests per `period`, per key, with a `burst` ceiling.
Under the token bucket a key starts with a full allowance of `burst`, spends one
unit per admitted request, and accrues `limit` units back over `period`.
`burst` defaults to `limit`; the window algorithms admit at most `limit` per
`period` and ignore it.

Constructors:

- `per_second(limit: u32) -> Quota` — infallible; `limit = 0` admits nothing.
- `per_minute(limit: u32) -> Quota` — infallible.
- `rate(limit: u32, period: Duration) -> Result<Quota, RateLimiterError>`  validated for arbitrary windows.

Accessors: `limit() -> u32`, `period() -> Duration`, `burst() -> u32`. Builder:
`with_burst(u32) -> Quota`.

```rust
use rate_net::Quota;
use std::time::Duration;

let per_sec = Quota::per_second(100);
assert_eq!(per_sec.limit(), 100);
assert_eq!(per_sec.period(), Duration::from_secs(1));

// Arbitrary window, validated.
let per_100ms = Quota::rate(5, Duration::from_millis(100))?;
assert_eq!(per_100ms.limit(), 5);
# Ok::<(), rate_net::RateLimiterError>(())
```

`rate` rejects values that cannot describe a working limit:

```rust
use rate_net::{Quota, RateLimiterError};
use std::time::Duration;

assert_eq!(Quota::rate(0, Duration::from_secs(1)), Err(RateLimiterError::ZeroQuota));
assert_eq!(Quota::rate(10, Duration::ZERO), Err(RateLimiterError::ZeroPeriod));
```

---

### `Eviction`

```rust
pub struct Eviction { /* private */ }
pub const DEFAULT_MAX_KEYS: usize; // 1 << 20
```

How the limiter bounds the memory its per-key state can occupy — the defense
against a unique-key flood. Two independent bounds compose: a **capacity** cap
(a hard ceiling on live keys; the least-recently-seen key is evicted to make
room) and an **idle TTL** (keys not seen for longer than the TTL are reclaimed).
Eviction is lazy, incremental, and per-shard — it never sweeps the whole store
or blocks the check path.

Constructors:

- `capacity(max_keys)` — a cap, no TTL.
- `idle(ttl)` — a TTL, keeping the [`DEFAULT_MAX_KEYS`] cap (idle expiry alone
  does not bound a flood, so the cap stays as the flood defense).
- `new(max_keys, ttl)` — both bounds.
- `unbounded()` — neither (only safe when the key space is intrinsically
  bounded).

Builders: `with_capacity(max_keys)`, `with_idle(ttl)`, `without_capacity()`.
Accessors: `max_keys() -> Option<usize>`, `idle_ttl() -> Option<Duration>`.

The [`Default`] is safe — a `DEFAULT_MAX_KEYS` cap and no TTL, so memory is
bounded out of the box.

A hard cap, the flood defense, with no idle expiry:

```rust
use rate_net::Eviction;

let policy = Eviction::capacity(100_000);
assert_eq!(policy.max_keys(), Some(100_000));
assert_eq!(policy.idle_ttl(), None);
```

Reclaim idle keys while keeping the default cap as the flood defense:

```rust
use rate_net::{Eviction, DEFAULT_MAX_KEYS};
use std::time::Duration;

let policy = Eviction::idle(Duration::from_secs(300));
assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(300)));
assert_eq!(policy.max_keys(), Some(DEFAULT_MAX_KEYS)); // still capped
```

Both bounds, chosen explicitly:

```rust
use rate_net::Eviction;
use std::time::Duration;

let policy = Eviction::capacity(50_000).with_idle(Duration::from_secs(60));
assert_eq!(policy.max_keys(), Some(50_000));
assert_eq!(policy.idle_ttl(), Some(Duration::from_secs(60)));
```

Unbounded — only when the key space is intrinsically small (a fixed tenant set):

```rust
use rate_net::Eviction;

let policy = Eviction::unbounded();
assert_eq!(policy.max_keys(), None);
```

The [`Default`] is safe — a [`DEFAULT_MAX_KEYS`] cap and no TTL — so memory is
bounded out of the box.

[`DEFAULT_MAX_KEYS`]: #eviction
[`Default`]: https://doc.rust-lang.org/std/default/trait.Default.html

---

### `Algorithm`

```rust
#[non_exhaustive]
pub enum Algorithm {
    TokenBucket,                              // always available; the default
    #[cfg(feature = "algorithms")] LeakyBucket,
    #[cfg(feature = "algorithms")] FixedWindow,
    #[cfg(feature = "algorithms")] SlidingWindowLog,
    #[cfg(feature = "algorithms")] SlidingWindowCounter,
}
```

Selects the algorithm a limiter applies; the selector the [`Builder`](#builder)
and [`with_algorithm`](#ratelimiterwith_algorithm) use. `#[non_exhaustive]` and
`Default` (`TokenBucket`). The four non-token variants exist only when the
`algorithms` feature is enabled.

```rust
use rate_net::Algorithm;

assert_eq!(Algorithm::default(), Algorithm::TokenBucket);
```

---

### `RateLimiterError`

```rust
#[non_exhaustive]
pub enum RateLimiterError {
    ZeroQuota,
    ZeroPeriod,
}
```

A limit configuration rejected at construction time, returned by
[`Quota::rate`](#quota). The check path never returns a `Result` — only
describing a limit can fail. Implements `std::error::Error`, `Display`, and
[`error_forge::ForgeError`](https://crates.io/crates/error-forge) (so `kind`,
`caption`, and `is_retryable` are available). `#[non_exhaustive]`.

- `ZeroQuota` — the quota limit was zero.
- `ZeroPeriod` — the quota period was zero.

```rust
use rate_net::{Quota, RateLimiterError};
use std::time::Duration;

let err = Quota::rate(0, Duration::from_secs(1)).unwrap_err();
assert_eq!(err, RateLimiterError::ZeroQuota);
assert!(err.to_string().contains("limit"));
```

---

### `Key`

```rust
pub struct Key(/* private */);
```

The opaque per-key identity a limit is tracked against — an IP, a user id, an API
token, a route. Stored as owned bytes — inline for keys up to a couple dozen
bytes, on the heap beyond that — so the common identities (IP addresses, `u64`
ids, short strings) cost no allocation and the steady-state check stays
allocation-free. Two keys are equal exactly when their bytes are equal, so the
identity is the byte content, not the source type. You rarely name it directly:
`check` accepts `impl Into<Key>`.

`From` conversions: `&str`, `String`, `&[u8]`, `Vec<u8>`, `u64`, `IpAddr`.
`as_bytes(&self) -> &[u8]` borrows the raw bytes.

```rust
use rate_net::Key;

let a: Key = "tenant:acme".into();
let b: Key = String::from("tenant:acme").into();
assert_eq!(a, b);
assert_eq!(a.as_bytes(), b"tenant:acme");
```

---

### `VERSION`

```rust
pub const VERSION: &str;
```

The crate version, captured from `Cargo.toml` at compile time — a
`major.minor.patch` string. Available even in `no_std` builds. Expose it to
report the exact `rate-net` build a process links against.

```rust
println!("rate-net {}", rate_net::VERSION);
assert!(rate_net::VERSION.starts_with("0."));
```

---

## Sharding and eviction

Per-key state lives in a **sharded** store: the key is hashed to one of a
power-of-two number of shards, each guarded by its own lock. An existing-key
check takes only a shard *read* lock and lets the key's bucket do its own atomic
accounting, so unrelated keys — and concurrent checks of the same key — never
serialise. Only first-seeing a key takes the brief write lock. Tune the shard
count with [`with_shards`](#ratelimiterwith_shards); it defaults to a small
multiple of the core count.

Memory is **bounded by eviction** (see [`Eviction`](#eviction)), the defense
against a unique-key flood. Eviction is lazy and per-shard: while inserting a new
key, under the write lock already held, the store drops idle-expired keys in that
one shard and, if the shard is at capacity, evicts its least-recently-seen key.
There is no background timer and no whole-store sweep, so the steady-state check
path is never blocked. The capacity cap is enforced approximately per shard, so
the live-key count stays within a small factor of the configured maximum.

```rust
use rate_net::{RateLimiter, Quota, Eviction};
use std::time::Duration;

let limiter = RateLimiter::with_quota(Quota::rate(1000, Duration::from_secs(60))?)
    .with_shards(64)
    .with_eviction(Eviction::capacity(100_000).with_idle(Duration::from_secs(300)));

assert_eq!(limiter.shards(), 64);
assert_eq!(limiter.eviction().max_keys(), Some(100_000));
# Ok::<(), rate_net::RateLimiterError>(())
```

These guarantees are verified by a `loom` model of the get-or-insert protocol, a
multi-threaded stress test (each key admitted exactly its quota under
contention), and an allocation audit (zero allocations on the steady-state
check).

---

## Tier 2 — the configured path

The [`Builder`](#builder) folds every knob — algorithm, quota, burst, shard
count, eviction policy, and clock — into one fluent surface, started with
[`RateLimiter::builder`](#ratelimiter). The same knobs are also chainable
adjusters ([`with_shards`](#ratelimiterwith_shards),
[`with_eviction`](#ratelimiterwith_eviction),
[`with_algorithm`](#ratelimiterwith_algorithm),
[`with_clock`](#ratelimiterwith_clock)) on a constructed limiter.

---

## Algorithms

All five share the [`Limiter`](#limiter-trait) surface and are selected by
[`Algorithm`](#algorithm). The token bucket is always available; the rest
require the `algorithms` feature. Each carries its own `proptest` over-admit
proof.

| Algorithm | Feature | Notes |
|-----------|---------|-------|
| Token bucket | always | Default; delegates to `better-bucket`. Bursts to capacity, then sustains. |
| Leaky bucket | `algorithms` | GCRA; spaces units at the emission interval, tolerating `burst`. |
| Fixed window | `algorithms` | Cheapest; lock-free packed counter. Tolerates a boundary burst (up to `2 × limit`). |
| Sliding-window log | `algorithms` | Exact; no boundary burst. Memory bounded by `limit` per key. |
| Sliding-window counter | `algorithms` | O(1) weighted two-window blend; approximate (worst case `2 × limit`). |

---

## Feature flags

| Feature | Default | Description |
|---------|---------|-------------|
| `std`        | yes | Standard library. Enables the limiter — the purpose-built sharded store, the token-bucket core (`better-bucket`'s `clock` feature), the injectable clock (`clock-lib`), and the error type (`error-forge`). With it off the crate is `no_std` and exposes only [`VERSION`]#version. |
| `algorithms` | no  | The leaky bucket and the window algorithms (fixed, sliding-log, sliding-counter), and their [`Algorithm`]#algorithm variants. The token bucket is always available without it. |
| `async`      | no  | The [`AsyncLimiter`]#asynclimiter await-until-ready wrapper. Additive; implies `std`. Only this layer touches a runtime (`tokio`'s timer). |

---

<sub>Copyright &copy; 2026 <strong>James Gober</strong>. All rights reserved.</sub>