Migrating to PyrateLimiter 4.0¶
This guide covers the breaking changes when upgrading from PyrateLimiter 3.x to 4.0.
Quick Summary¶
Version 4.0 simplifies the API by:
Removing exception-based flow control in favor of blocking behavior
Simplifying the decorator API
Adding proper async support with
try_acquire_asyncMoving clock responsibility from Limiter to Bucket
Adding context manager support
Breaking Changes¶
1. try_acquire is Now Blocking by Default¶
This is the most significant change. Previously, try_acquire would raise BucketFullException when the rate limit was exceeded. Now it blocks until a permit is available.
v3.x:
from pyrate_limiter import BucketFullException
try:
limiter.try_acquire("item")
except BucketFullException as e:
print(f"Rate limited: {e.meta_info}")
v4.0:
# Blocking (default) - waits until permit is available
limiter.try_acquire("item") # blocks until success when called without a timeout
# Non-blocking - returns immediately with False if bucket is full
success = limiter.try_acquire("item", blocking=False)
if not success:
print("Rate limited")
Base acquire signature (used by both sync try_acquire and async try_acquire_async):
def try_acquire(
name: str = "pyrate",
weight: int = 1,
blocking: bool = True, # NEW: wait for permit
timeout: int = -1 # NEW: max wait time (primarily used by try_acquire_async)
) -> bool
2. Use try_acquire_async for Async Code¶
For async code, use the new try_acquire_async method which uses asyncio.Lock and asyncio.sleep:
v3.x:
# v3.x used try_acquire for both sync and async
result = await limiter.try_acquire("item")
v4.0:
# Use try_acquire_async for proper async behavior
success = await limiter.try_acquire_async("item")
# With timeout (in seconds)
success = await limiter.try_acquire_async("item", timeout=5)
if not success:
print("Timed out waiting for permit")
# Non-blocking async
success = await limiter.try_acquire_async("item", blocking=False)
3. Decorator API Simplified¶
The decorator no longer requires a mapping function. Pass name and weight directly.
v3.x:
decorator = limiter.as_decorator()
def mapping(*args, **kwargs):
return ("item_name", 1) # (name, weight) tuple
@decorator(mapping)
def my_function():
pass
@decorator(mapping)
async def my_async_function():
pass
v4.0:
@limiter.as_decorator(name="item_name", weight=1)
def my_function():
pass
@limiter.as_decorator(name="item_name", weight=1)
async def my_async_function():
pass
4. Exception Classes Removed¶
BucketFullException and LimiterDelayException have been removed entirely. Use blocking=False to get non-blocking behavior.
v3.x:
from pyrate_limiter import BucketFullException, LimiterDelayException
try:
limiter.try_acquire("item")
except BucketFullException as e:
handle_rate_limit(e.meta_info)
except LimiterDelayException as e:
handle_delay_exceeded(e.meta_info)
v4.0:
success = limiter.try_acquire("item", blocking=False)
if not success:
handle_rate_limit()
5. Limiter Constructor Simplified¶
v3.x:
limiter = Limiter(
bucket,
clock=TimeClock(),
raise_when_fail=True,
max_delay=5000,
retry_until_max_delay=True,
)
v4.0:
limiter = Limiter(
bucket,
buffer_ms=50, # optional, default 50ms
)
Removed parameters:
clock- each bucket now manages its own clock viabucket.now()raise_when_fail- no exceptions are raised; useblocking=Falsemax_delay- blocking is controlled per-call viablockingparameterretry_until_max_delay- blocking mode retries automatically
6. BucketFactory Changes¶
If you implement a custom BucketFactory, remove the clock parameter from schedule_leak and create calls.
v3.x:
class MyFactory(BucketFactory):
def __init__(self, clock):
self.clock = clock
def wrap_item(self, name, weight=1):
return RateItem(name, self.clock.now(), weight=weight)
bucket = factory.create(clock, InMemoryBucket, rates)
factory.schedule_leak(bucket, clock)
v4.0:
class MyFactory(BucketFactory):
def wrap_item(self, name, weight=1):
return RateItem(name, self.bucket.now(), weight=weight)
bucket = factory.create(InMemoryBucket, rates)
factory.schedule_leak(bucket)
7. Clock Changes¶
Most users won’t need to change anything here - clocks are now managed internally by buckets.
If you explicitly used clock classes:
TimeClockremoved - buckets useMonotonicClockby defaultSQLiteClockremoved - buckets manage their own timeTimeAsyncClockrenamed toMonotonicAsyncClock
New Features¶
Context Manager Support¶
with Limiter(bucket) as limiter:
limiter.try_acquire("item")
# Resources automatically cleaned up
limiter_factory Module¶
Convenience functions for common patterns:
from pyrate_limiter import limiter_factory, Duration
limiter = limiter_factory.create_inmemory_limiter(
rate_per_duration=5,
duration=Duration.SECOND,
)
limiter = limiter_factory.create_sqlite_limiter(
rate_per_duration=100,
duration=Duration.MINUTE,
db_path="/path/to/db.sqlite",
)
Web Request Helpers¶
from pyrate_limiter.extras.aiohttp_limiter import RateLimitedSession
from pyrate_limiter.extras.httpx_limiter import RateLimiterTransport
from pyrate_limiter.extras.requests_limiter import RateLimitedRequestsSession
MultiprocessBucket¶
from pyrate_limiter import MultiprocessBucket
bucket = MultiprocessBucket(rates, manager)