# aiolimiter puts a leaky bucket behind an async with block

> aiolimiter is a small asyncio rate limiter built on the leaky bucket algorithm, used as an async context manager to cap how often a section of concurrent code is entered. It suits protecting an upstream API from your own client code, and it does not handle distributed limits across processes.

**mjpieters/aiolimiter** — An efficient implementation of a rate limiter for asyncio.

- Repository: https://github.com/mjpieters/aiolimiter
- Website: https://aiolimiter.readthedocs.io/
- Stars: 789 · Forks: 32
- Language: Python
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/mjpieters-aiolimiter

## One context manager and one leaky bucket

aiolimiter is described as an efficient implementation of a rate limiter for asyncio, and it implements the leaky bucket algorithm to give precise control over the rate a code section can be entered. The whole usage pattern fits in a few lines:

```python
from aiolimiter import AsyncLimiter

# allow for 100 concurrent entries within a 30 second window
rate_limit = AsyncLimiter(100, 30)


async def some_coroutine():
    async with rate_limit:
        # this section is *at most* going to entered 100 times
        # in a 30 second period.
        await do_something()
```

The semantics are worth reading carefully because they are not the same as a token bucket. The limit is expressed as a maximum number of entries within a window of seconds, and the comment in the example says the section is at most going to be entered 100 times in a 30 second period. The library also exposes a rate limit property in its documentation site at aiolimiter.readthedocs.io, which is where the adjustable parameters are described. The design goal is that the limit holds no matter how many tasks are waiting on the same limiter object, which is exactly the case a plain counter gets wrong.

## Why a leaky bucket rather than a token bucket

The choice of algorithm is the design decision behind the library, and it changes the behaviour you get. A leaky bucket produces a smooth, evenly spaced output: entries are permitted at a steady rate and the capacity of the bucket sets how much burst is tolerated. A token bucket refills continuously and lets a burst consume accumulated tokens at once. For protecting an upstream API, the leaky bucket's smooth rate is usually the safer default, because many third party APIs publish a quota expressed as a number of calls over a period rather than a peak requests per second, and a bursty client can exceed that average while still looking fine on a per second graph. The cost of the smooth behaviour is latency. A caller that arrives just after the bucket has emptied waits for the next slot instead of proceeding immediately, so the limiter pushes back on your own code rather than on the upstream.

## The context manager waits instead of rejecting

This is the point that most often decides whether the library fits your design. Entering the context manager blocks the calling task until the limiter permits entry. Nothing raises, nothing returns a failure, no error code is produced. That behaviour is deliberate and it is the right default for a client that must not exceed a quota, because the alternative is writing the retry logic yourself for every call site. But it has a direct operational consequence. If you have many concurrent tasks all waiting on one limiter, the latency of each one now includes the queueing delay inside the bucket, and your event loop holds that many pending tasks. If an upstream becomes slow and you hold many requests open at once, the limiter will happily let the next one in while the previous ones are still waiting, which means your real concurrency against the upstream is a separate variable the limiter does not control. Two independent limits, one for rate and one for concurrency, are usually what an HTTP client actually needs, and aiolimiter provides the first.

## A single package, no runtime dependencies

The dependency story is minimal, which suits a utility like this. pyproject.toml declares no runtime dependencies at all, only development and documentation groups. The project requires Python 3.10 or newer, and the classifiers list Framework :: AsyncIO with the intended audience of developers. The package is built with hatchling and uses dynamic versioning through uv-dynamic-versioning, with the version computed from git state rather than hardcoded. The documentation URL in the project metadata points at the stable version of the Read the Docs build, and the changelog is published both as a CHANGELOG.md file at the repository root and through towncrier fragments collected in a changelog.d directory. The source tree is laid out under src/, with tests/ alongside it. In short, the package you install brings no third party code with it, which means it will not conflict with the HTTP client or framework you already use.

## Installing the package and setting up the development tree

The install is a single pip command:

```bash
$ pip install aiolimiter
```

For contributors, the project uses uv to manage dependencies, testing and releases, and the setup is one command once uv is installed:

```bash
uv sync
```

The tests run with pytest and tox, releases are handled by a GitHub Actions workflow, code quality is maintained with ruff and pyright, and prek runs quick checks. A Taskfile is provided that defines the individual tasks, and listing them shows the shape of the workflow:

```console
$ task --list
task: Available tasks for this project:
* default:                               Default task, runs linters and tests
* dev:format:                            Runs formatters                           (aliases: format)
* dev:lint:                              Runs linters                              (aliases: lint)
* dev:test:                              Run tests                               (aliases: test)
* dev:tox:                               Run tests with tox                      (aliases: tox)
```

There are separate tasks for linting the Renovate configuration file, updating the uv lock file, building and cleaning and publishing distribution packages, and building, checking and serving the documentation, including a spelling check and a towncrier output check. The presence of a dedicated task for checking changelog towncrier output tells you the changelog fragments are enforced in CI rather than left to memory.

## A long gap between v1.2.1 and v1.3.0

The repository is not archived and the last push was on 2026-10-19, so the code is being worked on. The release history, though, has a shape that deserves a look. Aiolimiter 1.3.0 was published on 2026-09-07, while 1.2.1 dates from 2024-12-08 and 1.2.0 from 2024-12-01. That is a gap of close to two years between the two minor versions, and it tells you something about the risk profile of depending on it. A library this small and this stable does not need frequent releases, and a long quiet period is a reasonable signal. It also means that when 1.3.0 did arrive, it was the first change to the public behaviour in a long time, so the changelog for that release is the document to read before upgrading, and the CHANGELOG.md at the repository root is where it lives. The project also has an origin worth knowing about. It was first developed as an answer on Stack Overflow, and that original answer is linked from the README, which is a good place to read the author's own account of the design decisions.

## The limit is per process, not per deployment

The state that enforces the limit lives in the process that creates the AsyncLimiter object. That is the boundary you have to design around. If you run four worker processes behind a load balancer, each with its own limiter configured for 100 calls per 30 seconds, the upstream sees up to 400 calls in that window, and the upstream will throttle you as though a single client had broken the quota. Nothing in the library coordinates across processes, and the fix is not in the library but in your architecture. Either run a single limiter in a shared service, or configure each worker with a fraction of the quota, or let the upstream enforce the quota and use this library only for politeness. The same reasoning applies to restarts, since an in-memory bucket resets to empty when the process restarts. For a per-user or per-tenant quota rather than a global one, the pattern is one limiter instance per key, and the library documentation is the place to confirm how instances hold their own state.

## Conclusion

Adopt aiolimiter when your asyncio service calls an upstream that will throttle or block you, and when the limit you need applies per process, because the leaky bucket state lives in your own event loop. Do not adopt it for limits that must hold across several processes or machines, or as a substitute for a quota enforced by the upstream itself. Verify first that Python 3.10 or newer is available, since that is the stated requirement, that the upstream counts requests over the window you configure rather than per second, and that your workload can tolerate waiting inside the context manager instead of failing fast.

## FAQ

### How do I use aiolimiter to limit how often code runs?

Create an AsyncLimiter with a maximum number of entries and a window in seconds, for example AsyncLimiter(100, 30), then enter it with an async with block. The section inside is entered at most that many times within that period.

### Which Python versions does aiolimiter support?

The documentation states that the library requires Python 3.10 or newer, and the project metadata records requires-python as >=3.10.

### Does aiolimiter raise an error when the rate limit is reached?

No. The limiter waits rather than rejecting, so a task entering the context manager blocks until the algorithm permits entry. If you need fast failure instead of waiting, you have to check the rate limit property yourself before entering the block.

### Does aiolimiter share its rate limit state between processes?

The state lives inside the process that creates the limiter, so several worker processes each enforce their own allowance independently. To hold a quota across a deployment you need a single shared limiter or to divide the quota between workers.

### What algorithm does aiolimiter use?

The project implements the leaky bucket algorithm, which the documentation describes as giving precise control over the rate a code section can be entered, rather than a burst-oriented token bucket.

## Sources

- [License: MIT](https://github.com/mjpieters/aiolimiter/blob/main/LICENSE)
- [mjpieters/aiolimiter on GitHub](https://github.com/mjpieters/aiolimiter)
- [Project website](https://aiolimiter.readthedocs.io/)
- [README](https://github.com/mjpieters/aiolimiter/blob/main/README.md)
- [Releases](https://github.com/mjpieters/aiolimiter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mjpieters-aiolimiter
