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 via bucket.now()
  • raise_when_fail - no exceptions are raised; use blocking=False
  • max_delay - blocking is controlled per-call via blocking parameter
  • retry_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:

  • TimeClock removed - buckets use MonotonicClock by default
  • SQLiteClock removed - buckets manage their own time
  • TimeAsyncClock renamed to MonotonicAsyncClock

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)