Skip to content

Commit ea8a352

Browse files
committed
Rewrite strategy documentation to improve correctness
- Remove ambiguity around window start/end for fixed windows - Add consistent scenarios for all strategies
1 parent cdca682 commit ea8a352

File tree

3 files changed

+214
-97
lines changed

3 files changed

+214
-97
lines changed

README.rst

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,97 @@ limits
1616
|docs| |ci| |codecov| |pypi| |pypi-versions| |license|
1717

1818

19-
**limits** is a python library to perform rate limiting with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
19+
**limits** is a python library for rate limiting via multiple strategies
20+
with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
21+
22+
The library provides identical APIs for use in sync and
23+
`async <https://limits.readthedocs.io/en/stable/async.html>`_ codebases.
2024

2125

2226
Supported Strategies
2327
====================
28+
29+
All strategies support the follow methods:
30+
31+
- `hit <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.hit>`_: consume a request.
32+
- `test <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.test>`_: check if a request is allowed.
33+
- `get_window_stats <https://limits.readthedocs.io/en/stable/api.html#limits.strategies.RateLimiter.get_window_stats>`_: retrieve remaining quota and reset time.
34+
35+
Fixed Window
36+
------------
2437
`Fixed Window <https://limits.readthedocs.io/en/latest/strategies.html#fixed-window>`_
25-
This strategy resets at a fixed interval (start of minute, hour, day etc).
26-
For example, given a rate limit of ``10/minute`` the strategy will:
2738

28-
- Allow 10 requests between ``00:01:00`` and ``00:02:00``
29-
- Allow 10 requests at ``00:00:59`` and 10 more requests at ``00:01:00``
39+
This strategy is the most memory‑efficient because it uses a single counter per resource and
40+
rate limit. When the first request arrives, a window is started for a fixed duration
41+
(e.g., for a rate limit of 10 requests per minute the window expires in 60 seconds from the first request).
42+
All requests in that window increment the counter and when the window expires, the counter resets.
43+
44+
Burst traffic that bypasses the rate limit may occur at window boundaries.
3045

46+
For example, with a rate limit of 10 requests per minute:
3147

48+
- At **12:00:45**, the first request arrives, starting a window from **12:00:45** to **12:01:45**.
49+
- All requests between **12:00:45** and **12:01:45** count toward the limit.
50+
- If 10 requests occur at any time in that window, any further request before **12:01:45** is rejected.
51+
- At **12:01:45**, the counter resets and a new window starts which would allow 10 requests
52+
until **12:02:45**.
53+
54+
Moving Window
55+
-------------
3256
`Moving Window <https://limits.readthedocs.io/en/latest/strategies.html#moving-window>`_
33-
Moving window strategy enforces a rate limit of N/(m time units)
34-
on the **last m** time units at the second granularity.
3557

36-
For example, with a rate limit of ``10/minute``:
58+
This strategy records each request’s timestamp and counts only those requests within
59+
the last 60 seconds, creating a continuously sliding window based solely on recent
60+
activity.
61+
62+
For example, with a rate limit of 10 requests per minute:
3763

38-
- Allow 9 requests that arrive at ``00:00:59``
39-
- Allow another request that arrives at ``00:01:00``
40-
- Reject the request that arrives at ``00:01:01``
64+
- At **12:00:10**, a client sends 3 requests.
65+
- At **12:00:30**, the client sends 4 requests.
66+
- At **12:00:50**, the client sends 3 requests (total = 10).
67+
- At **12:01:10**, the system counts requests from **12:00:10** to **12:01:10**. Since all 10 requests
68+
are still within the window, a request at **12:01:10** is rejected.
69+
- At **12:01:11**, the 3 requests from **12:00:10** expire (window now has 7), so a new request is allowed.
4170

71+
Sliding Window Counter
72+
------------------------
4273
`Sliding Window Counter <https://limits.readthedocs.io/en/latest/strategies.html#sliding-window-counter>`_
43-
The sliding window counter strategy enforces a rate limit of N/(m time units)
44-
by approximating the moving window strategy, with less memory use. It approximates the behavior
45-
of a moving window by maintaining counters for two adjacent fixed windows: the current and the previous windows.
4674

47-
To determine if a request should be allowed, we assume the requests in the previous window were distributed evenly
48-
over its duration and use a weighted sum of the previous and current window counts to calculate the effective
49-
current capacity.
75+
This strategy approximates the moving window using less memory by maintaining two counters:
76+
77+
- **Current bucket:** counts requests in the ongoing period.
78+
- **Previous bucket:** counts requests in the immediately preceding period.
79+
80+
When a request arrives, the effective request count is calculated as::
81+
82+
weighted_count = current_count + (previous_count * weight)
83+
84+
The weight is based on how much time has elapsed in the current bucket::
85+
86+
weight = (bucket_duration - elapsed_time) / bucket_duration
87+
88+
If ``weighted_count`` is below the limit, the request is allowed.
89+
90+
For example, with a rate limit of 10 requests per minute:
91+
92+
Assume:
93+
94+
- The current bucket (spanning **12:01:00** to **12:02:00**) has 8 hits.
95+
- The previous bucket (spanning **12:00:00** to **12:01:00**) has 4 hits.
96+
97+
Scenario 1:
98+
99+
- A new request arrives at **12:01:30**, 30 seconds into the current bucket.
100+
- ``weight = (60 - 30) / 60 = 0.5``.
101+
- ``weighted_count = floor(8 + (4 * 0.5)) = floor(8 + 2) = 10``.
102+
- Since the weighted count equals the limit, the request is rejected.
103+
104+
Scenario 2:
50105

106+
- A new request arrives at **12:01:40**, 40 seconds into the current bucket.
107+
- ``weight = (60 - 40) / 60 ≈ 0.33``.
108+
- ``weighted_count = floor(8 + (4 * 0.33)) = floor(8 + 1.32) = 9``.
109+
- Since the weighted count is below the limit, the request is allowed.
51110

52111
Storage backends
53112
================
@@ -66,21 +125,27 @@ Initialize the storage backend
66125
.. code-block:: python
67126
68127
from limits import storage
69-
memory_storage = storage.MemoryStorage()
128+
backend = storage.MemoryStorage()
70129
# or memcached
71-
memcached_storage = storage.MemcachedStorage("memcached://localhost:11211")
130+
backend = storage.MemcachedStorage("memcached://localhost:11211")
72131
# or redis
73-
redis_storage = storage.RedisStorage("redis://localhost:6379")
132+
backend = storage.RedisStorage("redis://localhost:6379")
133+
# or mongodb
134+
backend = storage.MongoDbStorage("mongodb://localhost:27017")
74135
# or use the factory
75136
storage_uri = "memcached://localhost:11211"
76-
some_storage = storage.storage_from_string(storage_uri)
137+
backend = storage.storage_from_string(storage_uri)
77138
78-
Initialize a rate limiter with the Moving Window Strategy
139+
Initialize a rate limiter with a strategy
79140

80141
.. code-block:: python
81142
82143
from limits import strategies
83-
moving_window = strategies.MovingWindowRateLimiter(memory_storage)
144+
strategy = strategies.MovingWindowRateLimiter(backend)
145+
# or fixed window
146+
strategy = strategies.FixedWindowRateLimiter(backend)
147+
# or sliding window
148+
strategy = strategies.SlidingWindowCounterRateLimiter(backend)
84149
85150
86151
Initialize a rate limit
@@ -102,34 +167,34 @@ Test the limits
102167
.. code-block:: python
103168
104169
import time
105-
assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
106-
assert False == moving_window.hit(one_per_minute, "test_namespace", "foo")
107-
assert True == moving_window.hit(one_per_minute, "test_namespace", "bar")
170+
assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
171+
assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
172+
assert True == strategy.hit(one_per_minute, "test_namespace", "bar")
108173
109-
assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
110-
assert False == moving_window.hit(one_per_second, "test_namespace", "foo")
174+
assert True == strategy.hit(one_per_second, "test_namespace", "foo")
175+
assert False == strategy.hit(one_per_second, "test_namespace", "foo")
111176
time.sleep(1)
112-
assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
177+
assert True == strategy.hit(one_per_second, "test_namespace", "foo")
113178
114179
Check specific limits without hitting them
115180

116181
.. code-block:: python
117182
118-
assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
119-
while not moving_window.test(one_per_second, "test_namespace", "foo"):
183+
assert True == strategy.hit(one_per_second, "test_namespace", "foo")
184+
while not strategy.test(one_per_second, "test_namespace", "foo"):
120185
time.sleep(0.01)
121-
assert True == moving_window.hit(one_per_second, "test_namespace", "foo")
186+
assert True == strategy.hit(one_per_second, "test_namespace", "foo")
122187
123188
Query available capacity and reset time for a limit
124189

125190
.. code-block:: python
126191
127-
assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
128-
window = moving_window.get_window_stats(one_per_minute, "test_namespace", "foo")
192+
assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
193+
window = strategy.get_window_stats(one_per_minute, "test_namespace", "foo")
129194
assert window.remaining == 0
130-
assert False == moving_window.hit(one_per_minute, "test_namespace", "foo")
195+
assert False == strategy.hit(one_per_minute, "test_namespace", "foo")
131196
time.sleep(window.reset_time - time.time())
132-
assert True == moving_window.hit(one_per_minute, "test_namespace", "foo")
197+
assert True == strategy.hit(one_per_minute, "test_namespace", "foo")
133198
134199
135200
Links

doc/source/index.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
----
1919

20-
*limits* is a python library to perform rate limiting with commonly used
21-
storage backends (Redis, Memcached & MongoDB).
20+
**limits** is a python library for rate limiting via multiple strategies
21+
with commonly used storage backends (Redis, Memcached, MongoDB & Etcd).
2222

23+
The library provides identical APIs for use in sync and
24+
:ref:`async <async:async support>` codebases.
2325

2426
Get started by taking a look at :ref:`installation:installation` and :ref:`quickstart:quickstart`.
2527

0 commit comments

Comments
 (0)