@@ -16,38 +16,97 @@ limits
16
16
|docs | |ci | |codecov | |pypi | |pypi-versions | |license |
17
17
18
18
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.
20
24
21
25
22
26
Supported Strategies
23
27
====================
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
+ ------------
24
37
`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:
27
38
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.
30
45
46
+ For example, with a rate limit of 10 requests per minute:
31
47
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
+ -------------
32
56
`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.
35
57
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:
37
63
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.
41
70
71
+ Sliding Window Counter
72
+ ------------------------
42
73
`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.
46
74
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:
50
105
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.
51
110
52
111
Storage backends
53
112
================
@@ -66,21 +125,27 @@ Initialize the storage backend
66
125
.. code-block :: python
67
126
68
127
from limits import storage
69
- memory_storage = storage.MemoryStorage()
128
+ backend = storage.MemoryStorage()
70
129
# or memcached
71
- memcached_storage = storage.MemcachedStorage(" memcached://localhost:11211" )
130
+ backend = storage.MemcachedStorage(" memcached://localhost:11211" )
72
131
# 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" )
74
135
# or use the factory
75
136
storage_uri = " memcached://localhost:11211"
76
- some_storage = storage.storage_from_string(storage_uri)
137
+ backend = storage.storage_from_string(storage_uri)
77
138
78
- Initialize a rate limiter with the Moving Window Strategy
139
+ Initialize a rate limiter with a strategy
79
140
80
141
.. code-block :: python
81
142
82
143
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)
84
149
85
150
86
151
Initialize a rate limit
@@ -102,34 +167,34 @@ Test the limits
102
167
.. code-block :: python
103
168
104
169
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" )
108
173
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" )
111
176
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" )
113
178
114
179
Check specific limits without hitting them
115
180
116
181
.. code-block :: python
117
182
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" ):
120
185
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" )
122
187
123
188
Query available capacity and reset time for a limit
124
189
125
190
.. code-block :: python
126
191
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" )
129
194
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" )
131
196
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" )
133
198
134
199
135
200
Links
0 commit comments