# limits lets you swap the rate limiting strategy without rewriting the caller

> limits is a Python rate limiting library with three window strategies and four storage backends, an identical API surface for synchronous and asynchronous code, and a rate limit defined by a parseable string such as "1/minute".

**alisaifee/limits** — Rate limiting using various strategies and storage backends such as redis, memcached & mongodb

- Repository: https://github.com/alisaifee/limits
- Website: https://limits.readthedocs.org
- Stars: 651 · Forks: 84
- Language: Python
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/alisaifee-limits

## One interface over strategies and storage

limits is a Python library for rate limiting via multiple strategies, with storage backends that are the ones teams already run: Redis, Memcached and MongoDB, plus an in-memory option.

The design separates two decisions that usually get tangled. The first is what the limit should be, expressed as a rate limit item, and the second is how the accounting is done, expressed as a strategy. A strategy is constructed with a storage backend, and the strategies share the same three operations. Every strategy supports hit, which consumes a request, test, which checks whether a request is allowed without consuming it, and get_window_stats, which returns the remaining quota and the reset time.

That fixed set of three methods is what makes the strategies interchangeable. A caller that hits the limit and then reads the window statistics works the same way whether the accounting is one counter, a log of timestamps or two weighted counters. Changing the policy is a constructor argument, not a refactor of the request path.

The library also states that it provides identical APIs for use in sync and async codebases, with a separate async page in the documentation. That claim matters more than it sounds, because the usual outcome of a rate limiter is two libraries, one sync and one async, and code that has to be shared between a request handler and a background worker ends up maintaining both. The optional dependency list shows how that is carried: separate extras for the synchronous clients and separate ones for the asynchronous clients of each backend.

The audience is a Python web service that needs quotas. The library is not a gateway and it does not enforce anything by itself. It is a component you call.

## Fixed window: one counter and a burst at the boundary

The fixed window strategy is described as the most memory-efficient, because it uses a single counter per resource and rate limit. The window starts when the first request arrives and lasts for the fixed duration, so a limit of 10 requests per minute produces a window that expires 60 seconds after the first request rather than on the minute boundary. Requests inside that window increment the counter, and when the window expires the counter resets.

The cost of that memory saving is stated in the same breath: burst traffic that bypasses the rate limit may occur at window boundaries. The example makes it concrete. With 10 requests per minute, a request at 00:00:45 starts a window running to 00:01:45. All requests in that period count toward the limit, and if 10 occur, further requests are rejected until 00:01:45. At 00:01:45 the counter resets and a fresh window allows 10 more requests, right up to 00:02:45.

A client that times its requests to the minute can therefore send close to double the intended rate across a boundary, with the first burst at 00:00:45 and the second at 00:01:45. If your limit is protecting a downstream API with a burst allowance of zero, the fixed window is the wrong strategy, and the other two exist precisely because of this.

## Moving window: exact accounting from a log of timestamps

The moving window strategy trades memory for accuracy. Each request's timestamp is appended to a log, and a request is allowed when the nth oldest entry, where n is the limit, is either absent or older than the duration of the window. When a new entry is added, the expired entries are truncated.

The worked example in the documentation shows the difference from the fixed window at a glance. With 10 requests per minute, a client sends 1, then 2, then 4, then 3 requests, reaching 10 in total by 00:00:50. At 00:01:11 another request arrives, the strategy checks the timestamp of the 10th oldest entry, which is 00:00:10 and is now 61 seconds old, so the request is allowed. One second later, another request arrives and the 10th oldest entry is 00:00:20, only 52 seconds old, so the request is rejected.

So the moving window enforces the limit over a genuine trailing interval, with no boundary bonus, and a client that tries to spend its quota evenly gets exactly the behaviour it asked for.

The price is storage. The log holds one entry per request inside the window, which for a high-traffic key is a list that grows with the request rate and is trimmed only as entries expire. That is the trade the strategy makes, and it is why the sliding window counter exists.

## Sliding window counter: two counters and a weighted estimate

The sliding window counter approximates the moving window while using less memory, by keeping two counters. The current bucket counts the requests in the ongoing period, and the previous bucket counts the requests in the immediately preceding period. The effective count is a weighted blend:

```text
    weighted_count = current_count + floor(previous_count * weight)
```

The weight reflects how much of the current bucket has already elapsed:

```text
    weight = (bucket_duration - elapsed_time) / bucket_duration
```

If the weighted count is below the limit, the request is allowed. The examples make the arithmetic explicit. With 8 hits in the current bucket and 4 in the previous one, a request arriving 30 seconds into the bucket has a weight of 0.5, giving a weighted count of 10, which equals the limit and is rejected. Ten seconds later, at 40 seconds in, the weight is about 0.33, the weighted count becomes 9, and the request is allowed.

That last pair of examples is the whole point of the strategy. The same pair of requests is allowed or rejected depending on where in the bucket it lands, which is what removes the fixed window's boundary burst, while the storage cost stays at two counters regardless of traffic. The cost is precision: the previous bucket's contribution is estimated rather than counted, so the strategy is an approximation of the moving window and not an exact enforcement of it. Anyone whose limit is contractual rather than protective should know which of the two they are getting.

## Storage backends and the async split

The four storage backends are Redis, Memcached, MongoDB and in-memory. Each has a URL form, and the library can build a backend from a string with a factory as well as from a constructor, which matters when the storage choice comes from configuration rather than from code.

The optional dependencies make the mapping explicit. The synchronous extras are redis, rediscluster, memcached, mongodb and valkey, and the asynchronous ones are async-redis backed by coredis, async-memcached backed by memcachio, async-mongodb backed by motor, and async-valkey. The version constraints are visible in the packaging metadata, including the deliberate exclusion of specific redis releases, 4.5.2 and 4.5.3, from both the redis and rediscluster extras.

The in-memory backend is the one to think about first when you are evaluating, and it is also the one with a hard boundary. It holds counters in the process, which means the limit applies per process rather than per service. Two worker processes each get their own quota, so a deployment with eight workers allows eight times the intended rate. It is a development and single-process convenience, and treating it as a production limit is the most common way to end up with a limit that is not really a limit.

The other three backends are shared state, and each carries its own operational character. Memcached is the simplest to run and the most limited in what it can store, Redis is the common default for counters, and MongoDB suits teams whose infrastructure already runs it. The library also lists valkey in its topics and extras, which is the Redis-compatible fork, so a valkey deployment is a supported target rather than an experiment.

## Defining a limit and taking the first hit

A rate limit can be written as a string and parsed, which keeps the configuration human-readable:

```python
    from limits import parse
    one_per_minute = parse("1/minute")
```

Or it can be built explicitly from one of the item classes, which is the form to use when the limit is computed rather than written down:

```python
    from limits import RateLimitItemPerSecond
    one_per_second = RateLimitItemPerSecond(1, 1)
```

The storage backend is created first, and the strategy is created from the backend:

```python
   from limits import storage
   backend = storage.MemoryStorage()
   # or memcached
   backend = storage.MemcachedStorage("memcached://localhost:11211")
   # or redis
   backend = storage.RedisStorage("redis://localhost:6379")
```

Then the strategy, and the two alternatives that differ only in accounting:

```python
   from limits import strategies
   strategy = strategies.MovingWindowRateLimiter(backend)
   # or fixed window
   strategy = strategies.FixedWindowRateLimiter(backend)
   # or sliding window
   strategy = strategies.SlidingWindowCounterRateLimiter(backend)
```

The order matters more than it looks, because the rate limit item is what the strategy is asked about. The README's own quick example shows the shape of a call, hitting the strategy to consume a request.

For installation, the package is on PyPI and the documentation is at limits.readthedocs.org. The packaging metadata gives the details: it is built with hatchling and hatch-vcs, so the version comes from the repository tags rather than being written into the file, it requires Python 3.10 or later, and the classifiers cover 3.10 through 3.14 as well as PyPy. Only three dependencies are required, deprecated, packaging and typing_extensions, because every storage client is an extra you install yourself.

## The test matrix, the tooling and the release trail

The repository is organised around the idea that a rate limiter has to work against real servers, and the compose file shows the matrix the project tests against. There are four memcached variants, a plain one, a second on a different port, a Unix socket variant, and a SASL authenticated one that mounts a credentials file. There are Redis sentinel deployments with a master and a slave, a password protected pair with the same password on both, and a sentinel service itself, each on its own port. The environment variables for the server versions are templated, so the test matrix can be run against different server releases.

That is the honest test bed for this kind of library, because the failure modes are in the servers, not in the arithmetic. A sentinel failover, an expired memcached entry or a dropped MongoDB connection is a different situation from a counter in a dictionary, and the matrix exists to cover them.

The developer tooling is small and modern. The Makefile runs ruff for import sorting, linting and format checks, and mypy for type checking, all through uv, with a matching lint-fix target. The repository also carries a uv.lock, a devcontainer configuration, a pytest configuration and a coverage configuration, plus a readthedocs configuration and a netlify headers file for the documentation site.

The licence is MIT, with the licence text in LICENSE.txt, and the packaging metadata marks the development status as Production/Stable. Releases have come thick and fast over the last year: 5.6.0 on 2025-09-29, 5.7.0 on 2026-02-03 and 5.8.0 on 2026-02-05, two of them within a couple of days, and the last push to the repository was on 2026-10-21. A HISTORY.rst file records the changes, which is the file to read before pinning a version.

## Conclusion

limits fits a Python service that needs quotas shared across processes, where Redis, Memcached or MongoDB is already in the stack, and where the team wants to choose between a cheap fixed window and a stricter moving window without changing the call sites. It is the wrong choice for a single-process script that can hold a counter in a variable, since the storage abstraction buys nothing there, and it is the wrong choice if you need token bucket or leaky bucket behaviour, since the library ships window strategies only. Before adopting it, pick the storage backend that matches what you already run, confirm the client library for that backend is installed since each is an optional dependency, and read the moving window section of the docs if boundary bursts are a problem, because that is the strategy with the memory cost.

## FAQ

### Which rate limiting strategies does the limits library provide?

Three: Fixed Window, which uses a single counter and is the most memory-efficient, Moving Window, which logs each request timestamp for exact accounting, and Sliding Window Counter, which keeps two counters and weights the previous bucket's count to approximate the moving window. All three support hit, test and get_window_stats.

### Which storage backends can limits use?

Redis, Memcached, MongoDB and an in-memory backend, each constructed from a URL such as redis://localhost:6379 or built through the storage factory. The client libraries are optional dependencies, including coredis, memcachio and motor for the asynchronous variants, and valkey is supported as well.

### How do I define a rate limit in limits?

Either parse a string with limits.parse, for example parse("1/minute"), or construct an item class directly, for example RateLimitItemPerSecond(1, 1), which is the form to use when the limit is computed in code.

### Does limits work with async Python code?

Yes. The library states that it provides identical APIs for sync and async codebases, and the packaging metadata carries separate asynchronous extras for each backend, async-redis on coredis, async-memcached on memcachio, async-mongodb on motor, and async-valkey.

### Is the in-memory storage backend safe for production?

It holds the counters in the process, so the limit applies per process rather than per service, and a deployment with several workers would allow several times the intended rate. The shared backends, Redis, Memcached and MongoDB, are the ones that enforce a limit across a fleet.

## Sources

- [alisaifee/limits on GitHub](https://github.com/alisaifee/limits)
- [License: MIT](https://github.com/alisaifee/limits/blob/master/LICENSE)
- [Project website](https://limits.readthedocs.org)
- [README](https://github.com/alisaifee/limits/blob/master/README.md)
- [Releases](https://github.com/alisaifee/limits/releases)

---

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