Skip to content
Back to top
View this page

# 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_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)