I Felt Like a Clown Wiring 5 Libraries Just to Build a Resilient API Client
Source: Dev.to
So I wrote one that unifies everything.
I just wanted a simple API client.
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()
That lasted about 5 minutes.
Because real APIs:
- rate limit you
- timeout
- return 503
- sometimes completely die
- and retries can DDoS your own service
So I did what every Python dev does: I started stacking libraries.
The decorator tower of doom
First: rate limiting.
Then: retry.
Then: circuit breaker.
@breaker
@retry(...)
@sleep_and_retry
@limits(...)
async def fetch_user(...):
...
And I hated it.
Not because it didn’t work — but because it didn’t scale.
Problems
- 3+ libraries
- fragile decorator ordering
- conflicting abstractions
- async quirks
- painful testing
- scattered observability
- dependency sprawl
And this was for one function. Imagine 10 APIs, per‑user limits, background jobs, webhooks. You’re no longer writing business logic; you’re babysitting resilience glue code.
The idea: resilience as a pipeline
What if resilience wasn’t decorator soup? What if every call flowed through a single orchestrator?
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)
No decorators. No stacking libraries. No fragile glue. One execution pipeline. That’s what LimitPal is.
What LimitPal actually gives you
LimitPal is a toolkit for building resilient Python clients and services. It combines:
- rate limiting (token / leaky bucket)
- retry with exponential backoff + jitter
- circuit breaker
- composable limiters
- a resilience executor that orchestrates everything
And also provides:
- full async + sync support
- zero dependencies
- thread‑safe by default
- deterministic time control for tests
- key‑based isolation (per‑user / per‑IP / per‑tenant)
The goal isn’t more features. The goal is fewer moving parts.
The resilience pipeline (the key idea)
Every call goes through:
Circuit breaker check
→ Rate limiter
→ Execute + retry loop
→ Record result
The ordering matters. You’re not just “adding retry”; you’re designing failure behavior as a system:
- breaker stops cascading failures
- limiter protects infrastructure
- retry handles temporary issues
- executor keeps it coherent
One mental model instead of five.
The testing problem nobody talks about
Time‑based logic is brutal to test.
Traditional approach:
time.sleep(1)
Slow. Flaky. Non‑deterministic.
LimitPal uses a pluggable clock, so tests become:
clock.advance(1.0)
Instant. Deterministic. You can simulate minutes of retries in milliseconds. For teams that care about reliability, this is a game changer.
Real example
A resilient HTTP client in ~10 lines:
executor = AsyncResilientExecutor(
limiter=AsyncTokenBucket(capacity=10, refill_rate=100 / 60),
retry_policy=RetryPolicy(max_attempts=3, base_delay=0.5),
circuit_breaker=CircuitBreaker(failure_threshold=5),
)
async def fetch():
return await httpx.get("https://api.example.com")
result = await executor.run("api", fetch)
You automatically get:
- burst control
- exponential retry
- cascading failure protection
- clean async semantics
No decorator tower.
When should you use this?
Use LimitPal if you:
- build API clients
- call flaky third‑party services
- run background jobs
- need per‑user limits
- care about deterministic tests
- want clean async support
If you only need retry, smaller libs are fine. If you need composition, that’s the niche.
Part 2: internals
This post is about the idea. In Part 2 I’ll go deep into:
- how the executor pipeline works
- circuit breaker state machine
- clock abstraction design
- composite limiter architecture
- failure modeling
Because resilience isn’t magic. It’s architecture.
Try it
pip install limitpal
Docs:
Repo: