async-limiter
An asyncio rate limiter that enforces two limits at once:
how many requests run at the same time, and how many start per time period.
Many APIs publish both kinds of limit, for example "at most 5 requests in
flight and at most 100 per minute". DualRateLimiter holds a caller back until
both allow it. It has no dependencies.
pip install async-limiter
import asyncio
from async_limiter import DualRateLimiter
limiter = DualRateLimiter(max_concurrent=5, max_requests=100, time_period=60)
async def fetch(i):
async with limiter: # waits here until both limits allow the call
await asyncio.sleep(0.1) # your API call
return i
async def main():
results = await asyncio.gather(*[fetch(i) for i in range(20)])
print(results)
asyncio.run(main())
Three ways to use it
# As a context manager
async with limiter:
await call_api()
# As a decorator
@limiter.limit()
async def call_api_limited():
return await call_api()
# With explicit acquire and release
await limiter.acquire()
try:
await call_api()
finally:
limiter.release()
A limiter can be created at module level and shared by every task that calls the same API. Use one limiter per API.
Options
| Argument | Default | Meaning |
|---|---|---|
max_concurrent |
Most requests allowed in flight at once | |
max_requests |
Most requests allowed to start within time_period |
|
time_period |
Length of the window, in seconds | |
conservative |
True | Allow one request fewer than max_requests per window |
name |
None | Label used in log messages |
wait_margin |
0.1 | Extra seconds to sleep past the moment a slot opens |
How the limits behave
- The window slides. A request may start when fewer than
max_requestshave started in the lasttime_periodseconds, counted from each request's start. conservativeis on by default. Withmax_requests=100the limiter lets 99 through per window, to leave a margin for clock differences with the server. Passconservative=Falseto use the full limit.- Waiting adds a small margin. A caller that has to wait for the window
sleeps
wait_marginseconds (0.1 by default) past the moment a slot opens. On windows of a second or less this lowers throughput noticeably. - A waiting caller holds a concurrency slot. The concurrency limit is taken first, so callers queued on the time limit count as in flight.
- Cancellation is safe. A caller cancelled while waiting, for example by
asyncio.wait_for, gives its slot back. - One process, one event loop at a time. The limiter is not shared between
processes or threads;
acquire()raisesRuntimeErrorif another event loop is running with it. It can be reused by successiveasyncio.run()calls, and the request history carries over between them.
Using the full limit
Both safety margins are on by default. To get exactly the numbers you pass in:
limiter = DualRateLimiter(
max_concurrent=5,
max_requests=100,
time_period=60,
conservative=False, # allow 100 per window, not 99
wait_margin=0, # start as soon as a slot opens
)
Keep the margins when the limit is enforced by a server whose clock and counting you cannot see; drop them when you control both sides.
Metrics
limiter.get_metrics()
{
"total_requests": 100, # requests let through
"current_requests": 2, # holding a concurrency slot right now
"rate_limit_delays": 5, # requests that had to wait for the window
"total_delay_time": 12.5, # seconds spent in those waits
"errors": 0,
"current_queue_size": 3, # requests started within the current window
"max_concurrent": 5,
"max_requests_per_period": 99, # after the conservative adjustment
"time_period": 60,
}
limiter.reset_metrics() zeroes the counters.
Logging
The limiter logs to the rate_limiter logger: a line at INFO when it is
created and each time a caller has to wait, and details at DEBUG.
import logging
logging.getLogger("rate_limiter").setLevel(logging.WARNING) # quiet it down
Example
examples/api_client.py
fetches 40 resources from a public test API through one shared limiter and
prints its metrics. It needs aiohttp.
Project
Metadata
Release files for async-limiter 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| async_limiter-1.2.0.tar.gz | 11.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| async_limiter-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.8 kB
Release files / async_limiter-1.2.0.tar.gz
| Download URL | async_limiter-1.2.0.tar.gz |
|---|---|
| Size | 11.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8e37c23ca38451e518104d854f6b41b630a4686b6a30824e7453af7bc39dc860
|
|
BLAKE2b-256 checksum How to use checksums |
a5d47099d8552a8c9d312d9ad7016c0fbc25f74a872c97fea90645369085469c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / async_limiter-1.2.0-py3-none-any.whl
| Download URL | async_limiter-1.2.0-py3-none-any.whl |
|---|---|
| Size | 8.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0fbd64f95ce2547a966caef028faa77cd786f659f7bfe7296628b41293f4a5ef
|
|
BLAKE2b-256 checksum How to use checksums |
3cee33ea2367da20581987556a56fd0b2b895da9d1b0a38bb1071307e7af4776
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log