Open-source project
vutran1710/PyrateLimiter avatar
vutran1710/PyrateLimiter

PyrateLimiter: pluggable rate limiting for sync and async Python

⚔️Python Rate-Limiter using Leaky-Bucket Algorithm Family

529 stars53 forksPythonMIT

At a glance

What is it?
PyrateLimiter is a rate limiter for Python that separates the algorithm from the storage, so you can run an exact sliding window log over an in-memory bucket in a script and a constant memory token bucket over Redis in a service. The v4 line is a rewrite with a simpler API and breaking changes, and the project ships benchmarks, a Docker Compose file and examples for the HTTP clients people actually use.
Who is it for?
PyrateLimiter fits a service that needs per-key limits with a shared store across processes, because the same limiter object works with Redis or Postgres backends and the async paths do not block the event loop.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A rate limiter split into algorithm, storage and clock

PyrateLimiter is described as a fast, async-friendly rate limiter for Python with pluggable algorithms and backends, and the project name on PyPI is pyrate-limiter. It requires Python 3.10 or newer, ships type information, and is classified in its own metadata as Development Status :: 5 - Production/Stable, with classifiers for Python 3.10 through 3.14.

The architecture is the interesting part, and the documentation draws it explicitly. Four components carry the design.

The Limiter is the friendly façade, the public API, and it is sync and async, blocking or non-blocking, callable directly or usable as a decorator, and thread-safe through an RLock, with an asyncio.Lock in the async case.

The BucketFactory timestamps each item and routes it to the right bucket, and it schedules the background leaking.

The Bucket stores the timestamped items, enforces the rates and leaks out expired items.

The Clock is the time source, and its only requirement is a now() method returning an int, sync or async. That is a deliberately small interface, and it is what makes a custom or distributed clock possible.

The bucket analogy is the mental model the documentation uses. A bucket represents a fixed capacity, such as a service or an API quota. It fills as requests arrive and drains over time. When the bucket is full, new requests are either delayed, in the blocking case, or rejected, in the non-blocking case. What drains it, and at what rate, is the algorithm's business rather than the bucket's, which is exactly the split that lets the same bucket implementations serve different algorithms.

The project is by vutr, the licence is MIT, the documentation is at pyratelimiter.readthedocs.io, and the repository was last pushed on 2026-10-20, with v4.5.1 published the same day, after v4.5.0 on 2026-08-30 and v4.4.0 on 2026-06-14. The v4 line is a rewrite with a simpler API and breaking changes, and the documentation points to a migration guide for anyone coming from v3.

Installing, and the extras that carry the drivers

The install is one command, or a conda one:

sh
pip install pyrate-limiter
# or
conda install --channel conda-forge pyrate-limiter

The optional backends pull in their own drivers, and the extras are named after the backend rather than after a feature:

sh
pip install "pyrate-limiter[all]"        # redis + psycopg (Postgres) + filelock

Or one at a time:

sh
pip install "pyrate-limiter[redis]"      # RedisBucket, RedisStateStore
pip install "pyrate-limiter[postgres]"   # PostgresBucket
pip install "pyrate-limiter[filelock]"   # multi-process SQLite

The metadata lists the extras as redis with redis 6.2.0 or newer, postgres with psycopg pool 3.2.9 or newer, and filelock with filelock 3.0 or newer, and the all extra is the union of the three.

There is a revealing comment in the project metadata about these extras. The dependency-groups section is described as PEP 735 and as being for development only, with the note that pip cannot reach them through extras syntax, which is why the install command in the README used to resolve to nothing. In other words, the documented command was silently installing nothing until the extras were defined properly, and the comment is there so that nobody moves the groups back into the extras block.

That is a small thing, but it is the kind of detail that matters in a library whose whole job is to sit between you and an external system, where a missing driver shows up as an import error at the moment you first use a backend rather than at install time.

The quickstart, and a one-liner for the common case

The quickstart limits to five requests per two seconds, with a limiter backed by an in-memory bucket:

python
from pyrate_limiter import Duration, Rate, Limiter

# A Limiter with a single rate, backed by an in-memory bucket
limiter = Limiter(Rate(5, Duration.SECOND * 2))

# Blocking (default): waits until a permit is available
for i in range(6):
    limiter.try_acquire("my-resource")
    print(f"acquired {i}")

# Non-blocking: returns False immediately when the bucket is full
if not limiter.try_acquire("my-resource", blocking=False):
    print("rate limited!")

Two modes in one example, and the difference matters for a server. Blocking is the default, so try_acquire waits until a permit is available, which is the right behaviour for a background worker that must not lose work. Non-blocking returns False immediately when the bucket is full, which is the right behaviour for an HTTP handler that should answer with an error rather than hold a connection open.

For the common case there is a factory that avoids spelling out the rate and the bucket:

python
from pyrate_limiter import Duration, limiter_factory

limiter = limiter_factory.create_inmemory_limiter(rate_per_duration=5, duration=Duration.SECOND)
limiter.try_acquire("my-resource")

The key argument is the resource identifier, and per-key limits are a feature in their own right: different services, users or resources are tracked independently, so a limit of five per second applies to each key rather than to the process.

Three algorithms and what each one allows

The algorithm choice is the part with real consequences, because the three options do not allow the same bursts.

The default is a sliding window log, which the documentation calls exact. Every request is stored as a timestamp and a request stops counting once it falls out of the rolling window. The cost is memory proportional to the number of requests inside the window, and the benefit is that the behaviour is precisely what the limit says.

A fixed window is the second option, where the count resets on a boundary. That is cheaper in memory and cheaper in time, and it has the well known property that a client can send a full allowance at the end of one window and another at the start of the next, so the effective rate over a short interval is higher than the nominal limit.

The third is GCRA, also called a token bucket in the documentation, and it is described as constant memory. Here the bucket genuinely leaks at a constant rate, which the documentation says makes it the leaky bucket algorithm proper, as opposed to the default where a request merely stops counting when it leaves the window. The rest of the API is the same across algorithms, which is the point of the pluggable design.

Multiple rates at once are supported on the same key, for example five per second and a thousand per hour, which is how a real API quota is usually expressed. The bucket enforces the rates, and which algorithm you pick determines how strictly a burst is smoothed.

Decorators, context managers, weights and the web request case

The everyday usage section of the documentation covers the ways a limiter is used in real code, and the headline is that a limiter can be used directly or as a decorator, with the decorator form referenced as limiter.as_decorator.

There is a context manager form, and a weight mechanism, which is how a request that costs more than one unit of quota is accounted for. Weight is the feature that makes a limiter usable for a job queue as well as for HTTP: a small request and a large one can cost different amounts against the same bucket.

Blocking, non-blocking and timeout are the three outcomes, and timeout is the middle case, waiting up to a limit and then giving up, which is what you want in a request handler that must answer within a deadline.

The asyncio section is about the event loop, and the promise is that the async paths never block the event loop. The examples directory makes the practical part concrete, with files for aiohttp, httpx, httpx2, requests, asyncio decorators, plain asyncio, Redis, Postgres, token bucket, in-memory multiprocess and SQLite with a file lock.

The web request rate limiting section is the one to read if you are putting this behind an API. The limiter is keyed by whatever identifies the caller, and the blocking or non-blocking choice determines whether the client sees a slow response or a rejection, which is a product decision as much as a technical one.

Advanced ground: custom routing, custom clocks and custom backends

The advanced section covers the parts of the design that exist because real deployments are more complicated than one process with one bucket.

Custom routing with BucketFactory is the mechanism for deciding which bucket an item goes to, which is what lets one limiter enforce different rates for different kinds of key, for example a strict limit for one endpoint and a loose one for another, without creating two limiters.

Custom and distributed clocks matter when the limiter runs in more than one process. The Clock interface only needs now() returning an int, so a clock that reads a shared time source, or that returns a monotonically increasing counter, can be substituted. That is the honest answer to the problem of a rate limit that has to be consistent across several machines: the algorithm still has to be the same on each machine, but the time source can be shared.

Leaking is the background cleanup that removes expired items from buckets, and the documentation explains how to control it. The leaker runs periodically in the background, and a bucket that is never cleaned keeps the timestamps of every request it has seen.

Concurrency is addressed with the locks mentioned earlier, an RLock for the sync path and an asyncio.Lock for the async one, and custom backends are the extension point for anyone whose storage is not in the list. The built-in choices are in-memory, SQLite, Redis in both sync and async form, Postgres and multiprocessing, and the multiprocess option is the one that pairs with the filelock extra for a multi-process SQLite setup.

The clock is worth one more word, because it is the component most libraries hide and this one exposes. A rate limiter that trusts the system clock will misbehave across a clock adjustment, and a limiter that has to run on three machines needs those machines to agree about what time it is.

Benchmarks with CPU pinning, and how the releases are made

The repository carries a benchmarks/ directory and a docker-compose.yaml with a Redis master and a Redis replica, a Postgres service on port 5432 with a raised max_connections, and a bridge network, and the Makefile contains a benchmark harness that is unusually careful about measurement.

The setup target refuses to run anywhere but Linux, because it depends on taskset and Docker CPU pinning, and it checks that git, docker, taskset and uv are all present, that the Docker daemon is reachable, and that the two CPU identifiers it has been given differ. It then creates git worktrees for the baseline and the candidate, so the two versions being compared are built from separate checkouts, and the default goal is the Redis benchmark. There is a note that the default branch is selected as the baseline and the committed HEAD as the candidate, with overrides for both, and the environment variables for the CPU assignment and the number of rounds are all overridable from the command line.

The rest of the tooling is conventional and modern: a noxfile.py for task automation, a uv.lock for reproducible environments, a .pre-commit-config.yaml, a Makefile, a CHANGELOG.md, a CONTRIBUTING.md and a RELEASING.md. The examples directory, the tests directory and the docs directory sit alongside the package.

For an adopter, the important point is that the project measures itself, and that the measurement setup is built to be trustworthy. A rate limiter's performance characteristics are dominated by the backend round trip, and a benchmark that ran the two versions on different CPUs or with a warm or a cold Redis would tell you nothing.

Editorial conclusion

PyrateLimiter fits a service that needs per-key limits with a shared store across processes, because the same limiter object works with Redis or Postgres backends and the async paths do not block the event loop. It is the wrong tool if you only need to throttle one process, where the in-memory bucket is enough and a network round trip is a cost you do not need to pay, and it is the wrong tool if you are still on v3, since v4 changed the API and the project provides a migration guide. Before you upgrade, read the migration guide, decide which algorithm matches the behaviour you are trying to reproduce, since a sliding window and a token bucket allow different bursts, and check the extras you install, because the optional backends pull their own drivers and the project has already had one packaging bug where an install command resolved to nothing.

Frequently asked questions

What is PyrateLimiter and which algorithms does it support?

PyrateLimiter is a rate limiter for Python 3.10 and newer with pluggable algorithms and pluggable storage backends. The algorithms are a sliding window log, which is the default and is exact, a fixed window, and GCRA, also called a token bucket, which uses constant memory and leaks at a constant rate.

How do I install PyrateLimiter and the optional backends?

Install it with pip install pyrate-limiter, or with conda install --channel conda-forge pyrate-limiter. The optional backends are extras, with pip install "pyrate-limiter[redis]" for the Redis bucket and state store, pip install "pyrate-limiter[postgres]" for the Postgres bucket, pip install "pyrate-limiter[filelock]" for multi-process SQLite, and pip install "pyrate-limiter[all]" for all of them.

What is the difference between blocking and non-blocking mode in PyrateLimiter?

Blocking is the default and waits until a permit is available, which suits a worker that must not lose work. Non-blocking returns False immediately when the bucket is full, which suits an HTTP handler that should reject rather than hold a connection open. A timeout mode waits up to a limit and then gives up.

How does PyrateLimiter work across multiple processes or machines?

The bucket implementations can be backed by Redis, Postgres or a multi-process SQLite file lock, and the Clock component is a separate time source that only needs a now method returning an integer, so a custom or shared clock can be substituted. The documentation also covers custom routing with BucketFactory, background leaking, concurrency locks and writing your own backend.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. vutran1710/PyrateLimiter on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vutran1710-pyratelimiter.svg)](https://hysenlabs.com/projects/vutran1710-pyratelimiter)

Was this useful?