Migrating to PyrateLimiter 4.0
December 23, 2025 ยท View on GitHub
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_async - Moving 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)