Поэтому я написал одну, которая объединяет всё.
Когда простой API-клиент превращается в зоопарк
Любой проект начинается с чего-то такого:
import httpx async def fetch_user(user_id: str): async with httpx.AsyncClient() as client: r = await client.get(f"https://api.example.com/users/{user_id}") return r.json()
А потом реальность:
API лимитирует запросы
иногда отвечает 503
иногда виснет
иногда падает полностью
и retry может убить уже ваш сервис
И начинается подключение библиотек:
rate limit
retry
circuit breaker
И функция превращается в это:
@breaker @retry(...) @sleep_and_retry @limits(...) async def fetch_user(...): ...
Формально всё работает.
Но появляется ощущение, что ты не пишешь бизнес-логику.
Ты пишешь инфраструктурный клей.
Главная проблема — не в библиотеках
Каждая библиотека сама по себе нормальная.
Проблема в композиции:
разные модели времени
разные абстракции ошибок
разные API
сложное тестирование
непредсказуемое поведение при нагрузке
Retry + limiter + breaker — это уже система.
А мы собираем систему из несвязанных кусочков.
Идея: сделать устойчивость конвейером
Вместо “башни декораторов” я хотел одно:
единый pipeline выполнения вызова
Чтобы каждый запрос проходил через понятную схему:
Circuit breaker → Rate limiter → Retry loop → Запись результата
Без магии.
Без хаоса.
Без glue-кода.
Получилось вот так:
from limitpal import ( AsyncResilientExecutor, AsyncTokenBucket, RetryPolicy, CircuitBreaker ) executor = AsyncResilientExecutor( limiter=AsyncTokenBucket(capacity=10, refill_rate=100/60), retry_policy=RetryPolicy(max_attempts=3), circuit_breaker=CircuitBreaker(failure_threshold=5) ) result = await executor.run("user:123", api_call)
Одна точка входа.
Одна модель поведения.
Одна архитектура.
Почему это важно
Retry без limiter — опасен.
Limiter без breaker — слеп.
Breaker без retry — жёсткий.
Только вместе они образуют стратегию устойчивости.
Именно стратегия, а не набор фич.
Тестирование — скрытая боль
Самая неприятная часть таких систем — время.
Традиционно:
time.sleep(1)
Медленно.
Флейково.
Непредсказуемо.
Поэтому внутри используется управляемые часы:
clock.advance(1.0)
Тесты выполняются мгновенно, но поведение идентично реальному.
Это звучит как мелочь.
На практике — спасение для CI и нервов.
Что в итоге получилось
Я собрал всё в одну библиотеку — LimitPal.
Это не “ещё один rate limiter”.
Это попытка сделать устойчивость архитектурным примитивом:
rate limiting
retry с backoff и jitter
circuit breaker
композиция стратегий
sync + async API
детерминированные тесты
Без внешних зависимостей.
Когда это имеет смысл
Если вы:
пишете API-клиенты
ходите во внешние сервисы
делаете фоновые джобы
боитесь retry-штормов
хотите предсказуемое поведение под нагрузкой
тогда архитектурный подход окупается.
Если нужен только retry — можно взять маленькую либу.
Но как только появляется композиция — начинается системная инженерия.
Документация:
https://limitpal.readthedocs.io/
Репозиторий:
https://github.com/Guli-vali/limitpal
Если идея зашл — буду рад обратной связи.