mrkeyoor.com_
Sun 20 Sept 17:48 UTC
PyPIUtilsupdated 20 Sept 2026

limits review

limits 5.8.0 is a rate-limit engine for Python code that already knows what should be limited and what to do when capacity runs out. It parses rules such as `100/minute`, applies fixed-window, moving-window, or sliding-counter math, and stores counters in memory, Redis, Valkey, Memcached, or MongoDB. Sync and asyncio modules expose the same three decisions: consume with `hit()`, look ahead with `test()`, and read quota state with `get_window_stats()`. HTTP decorators, client identification, 429 responses, and headers belong to a framework wrapper. The current release adds explicit storage credentials and Redis Cluster startup nodes, including IPv6 locators.

Verdict

limits 5.8.0 installed in 0.3 seconds and occupied 2 MB across 5 packages, with one deprecation warning and no audit findings in our sandbox. Use it when your code owns the quota key, storage, and exhaustion response; use an HTTP wrapper or token-bucket library when those are the actual requirements.

We installed it

Lab card: what happened when we installed limitsScreenshot of limits documentation
Install✓ · 0.3s5 packages on disk · 2 MB · 1 deprecation warning
Importimport limits in 0.41s · pure Python · py.typed · requires Python >=3.10
Known vulns0(pip-audit)

Answers from our run

Does limits install cleanly?

Yes. In a fresh container with an empty cache, pip install limits finished in 0.3s, leaving 5 packages and 2 MB on disk. pip-audit reported no known vulnerabilities. The install printed 1 deprecation warning.

What does limits need to run?

Python >=3.10, and nothing compiled: it is pure Python. In our run import limits succeeded in 0.41s, and the package ships py.typed for type checkers.

limits or Flask-Limiter: which should you use?

Pick Flask-Limiter when a Flask app needs decorators, key functions, exemptions, response headers, and automatic 429 handling. limits 5.8.0 installed in 0.3 seconds and occupied 2 MB across 5 packages, with one deprecation warning and no audit findings in our sandbox.

When should you not use limits?

Routes need decorators, automatic client keys, exemptions, headers, and 429 responses. Flask-Limiter or SlowAPI supplies that missing HTTP layer.

API stability4/5The strategy contract remains centered on `hit()`, `test()`, and `get_window_stats()`, with parallel sync and asyncio modules. Release 5.8.0 changed storage configuration by accepting credentials, Redis Cluster startup nodes, and IPv6 locators without replacing those limiter calls. Backend client ranges and the minimum Python version can still move between major releases, so a real storage integration test is part of an upgrade rather than an optional check.
Docs4/5Official pages explain all 3 window algorithms with timelines and calculations, then document parsing, storage classes, strategy methods, and asyncio equivalents. That is enough to understand why fixed windows burst and why sliding counters are approximate. Deployment details are scattered: backend feature differences, Redis proxy restrictions, connection ownership, and framework behavior may require the storage reference, source, and a wrapper's separate manual.
Maintenance5/5Version 5.8.0 was published on 2026-02-05, and repository work continued through 2026-08-05. Its fixes cover explicit cluster startup nodes, storage credentials, and IPv6 Redis Cluster locators, all concrete operational cases beyond the in-memory path. GitHub currently counts 14 open issues and pull requests together. The repository is active, and the README exposes both CI and coverage results.
Ecosystem4/5GitHub shows 642 stars, while the package is also the counter layer beneath framework integrations such as Flask-Limiter and SlowAPI. Direct callers can choose Redis, Valkey, Memcached, MongoDB, or process memory and can use sync or asyncio APIs. That breadth creates useful integration paths, but examples found in search may belong to a wrapper and cannot be copied into the lower-level `limits` API unchanged.

Use it if

  • Queue jobs, outbound calls, account actions, or custom middleware need one quota engine without a web-framework dependency.
  • Several workers must share counters through Redis, Valkey, Memcached, or MongoDB.
  • Your policy fits a fixed window, an exact moving timestamp log, or an approximate sliding counter.
  • Application code needs remaining capacity and reset time so it can build its own wait policy, headers, or metrics.
Skip it if

Setup reality

We installed limits 5.8.0 in a clean Python 3.12 Bookworm container. The install finished in 0.3 seconds, put 5 packages and 2 MB on disk, and printed one deprecation warning. import limits took 0.41 seconds, while pip-audit found zero known vulnerabilities. Package metadata lists 12 direct dependencies and Python 3.10 or newer. The distribution is pure Python, carries py.typed, and declares the MIT License.

MemoryStorage works immediately, but its counters disappear with the process and cannot coordinate multiple workers. Redis, Valkey, Memcached, and MongoDB each need the corresponding extra, service, URI, and credentials. Sync and async imports do not share every client package. Build the backend at process startup so a missing extra or bad connection fails before the first limited operation. Release 5.8.0 lets supported storages receive username and password as keyword options.

Algorithm choice changes accuracy and state. A fixed window holds one counter and admits bursts at the boundary. A moving window keeps request timestamps for an exact rolling decision. The sliding counter combines 2 buckets, trading precision for less stored data. Each backend implements a particular capability set, so an integration test must cover the exact storage and strategy pair, including any Redis proxy or cluster.

Treat hit() as the final permission check because the backend tests and consumes in one operation. test() spends nothing, so another worker can win the capacity before a later call to hit(). The limit plus every supplied identifier forms the storage key; changing 5/minute to 10/minute starts addressing another key. get_window_stats() reports an absolute reset timestamp and remaining capacity. Your application still has to translate that into its own wait behavior or HTTP headers.

Patterns

Parse configured quotas parse-limit

from limits import parse, parse_many

per_user = parse('100/minute')
tiers = parse_many('5/second; 100/minute; 1000/hour')

Invalid quota text raises `ValueError`. Parse configuration during startup so a typo cannot wait until the first request or job.

Consume a fixed-window quota fixed-window-hit

from limits import parse
from limits.storage import MemoryStorage
from limits.strategies import FixedWindowRateLimiter

limiter = FixedWindowRateLimiter(MemoryStorage())
quota = parse('100/minute')
allowed = limiter.hit(quota, 'user', user_id)

`MemoryStorage` belongs to one process, so a second worker gets another counter. A fixed window can admit traffic on both sides of its reset boundary.

Use an exact moving window in Redis redis-moving-window

from limits import parse
from limits.storage import RedisStorage
from limits.strategies import MovingWindowRateLimiter

storage = RedisStorage('redis://localhost:6379/0')
limiter = MovingWindowRateLimiter(storage)
allowed = limiter.hit(parse('10/minute'), 'api-key', api_key)

A moving window records request times and only works on supporting backends. Exercise its Redis commands through the same proxy or cluster used in production.

Use an approximate sliding counter sliding-window-counter

from limits import parse
from limits.storage import storage_from_string
from limits.strategies import SlidingWindowCounterRateLimiter

storage = storage_from_string('redis://localhost:6379/0')
limiter = SlidingWindowCounterRateLimiter(storage)
allowed = limiter.hit(parse('1000/minute'), 'tenant', tenant_id)

This algorithm weights the previous bucket against the current one. Its 2 counters use less state than a timestamp log and produce an approximation.

Construct storage from configuration build-storage-from-uri

from limits.storage import storage_from_string

storage = storage_from_string(
    'redis://:secret@redis.internal:6379/1'
)

Construct the backend at startup. A valid-looking URI still fails if the optional client is missing, credentials are wrong, or the server is unreachable.

Check capacity before expensive work peek-without-consuming

if limiter.test(quota, 'tenant', tenant_id, cost=estimated_cost):
    prepare_request()

if not limiter.hit(quota, 'tenant', tenant_id, cost=actual_cost):
    raise QuotaExceeded()

`test()` does not reserve capacity. Another worker may consume the last unit before `hit()`, making `hit()` the only authoritative decision.

Charge a request by token count charge-variable-cost

token_budget = parse('10000/hour')

if not limiter.hit(
    token_budget,
    'llm-tokens',
    tenant_id,
    cost=token_count,
):
    raise QuotaExceeded()

The `cost` argument spends several units in one hit. A request larger than the entire quota cannot pass until the policy itself changes.

Build quota response data read-window-stats

import time

window = limiter.get_window_stats(quota, 'user', user_id)
response_headers = {
    'X-RateLimit-Remaining': str(window.remaining),
    'X-RateLimit-Reset': str(int(window.reset_time)),
    'Retry-After': str(max(1, int(window.reset_time - time.time()))),
}

`reset_time` is an absolute Unix timestamp. An HTTP `Retry-After` delay is relative seconds, so calculate it instead of copying that value.

Consume quota in asyncio code async-redis-limit

from limits import parse
from limits.aio.storage import RedisStorage
from limits.aio.strategies import MovingWindowRateLimiter

storage = RedisStorage('async+redis://localhost:6379/0')
limiter = MovingWindowRateLimiter(storage)

allowed = await limiter.hit(parse('50/minute'), 'user', user_id)

Both classes must come from `limits.aio`. The async Redis extra follows a different client dependency path from the synchronous adapter.

Keep two equal quotas separate separate-namespaces

from limits import RateLimitItemPerMinute

login = RateLimitItemPerMinute(5, namespace='login')
password_reset = RateLimitItemPerMinute(5, namespace='password-reset')

limiter.hit(login, 'ip', client_ip)
limiter.hit(password_reset, 'ip', client_ip)

Namespace and identifiers participate in the stored key. Equal numeric limits share capacity only when those key components also match.

Clear a caller's limit clear-one-counter

quota = parse('5/minute')
limiter.clear(quota, 'login', user_id)

`clear()` needs the exact quota and identifiers passed to `hit()`. A different amount or namespace points at a different stored counter.

Close storage during shutdown close-storage

async def shutdown():
    await storage.close()

Shutdown differs between sync and async storage classes. A client pool supplied by the application may have to outlive the limiter instead of closing here.

Alternatives

PackageRegistryPick it when
Flask-LimiterPyPIPick it when a Flask app needs decorators, key functions, exemptions, response headers, and automatic 429 handling.
slowapiPyPIPick it for rate limits attached directly to FastAPI or Starlette routes.
pyrate-limiterPyPIPick it when bucket refill, scheduling, and bursts describe the policy better than window counters.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.