HTTP/1.1 and HTTP/2 with transparent ALPN negotiation, structured concurrency, built-in resilience, and security-conscious defaults — without ever exposing its transport in the public API.
Butler owns its own public API — Client, Request, Response,
Headers — built around Ruby's Fiber::Scheduler rather than one Thread per
in-flight request. That's the whole point of the design: real HTTP/2 multiplexing, structured
concurrency you can actually reason about (client.async { |tasks| ... }), and a
resilience layer (timeouts, deadlines, retries with backoff+jitter, a circuit breaker) that's part
of the client, not four extra gems bolted onto Net::HTTP.
gem install butler-http
# HTTParty-style, zero setup:
Butler.get("https://api.example.com/users").json
# Or a configured Client, for connection pooling/retries/middleware tuned per-API:
client = Butler::Client.new(base_url: "https://api.example.com")
client.async do |tasks|
users = tasks.async { client.get("/users") }
orders = tasks.async { client.get("/orders") }
{ users: users.wait.json, orders: orders.wait.json }
end
http_version: override when you need to force
one or the other.client.async { |tasks| tasks.async { ... } }
gives real parent/child task cancellation and exception propagation, backed by an
Async::Barrier, not a bare Thread pool.deadline: that survives every
retry and redirect hop, exponential backoff with jitter, a CLOSED → OPEN → HALF_OPEN
circuit breaker, and retry rules that actually respect HTTP semantics (POST is never
auto-retried on a 5xx).| Capability | Butler | Faraday | Excon | Net::HTTP |
|---|---|---|---|---|
| Fiber-native concurrency | ✓ | — | — | limited |
| HTTP/2 (ALPN, multiplexed) | ✓ (first-class) | adapter-dependent | limited | depends |
| Connection pooling | ✓ | adapter | ✓ | manual |
| Retries + backoff/jitter | built-in | middleware | limited | manual |
| Circuit breaker | built-in | external gem | external gem | external gem |
| Deadlines (total budget across retries) | ✓ | — | — | — |
| OpenTelemetry | first-class, optional | external | external | external |
| Native request stubbing | ✓ | via WebMock | via WebMock | via WebMock |
The one architectural rule that keeps this from rotting into "Faraday but slower": Butler owns
its public abstractions. async/async-http implement the real transport
underneath, but nothing outside lib/butler/transport.rb ever touches those types —
the public API doesn't change if the transport underneath it ever does. Full breakdown, including
Excon/HTTParty and a real Sequential/Concurrent benchmark matrix, in the
README.
Fiber::Scheduler actually is, thread-per-request vs. Fiber-per-request, and
why it fits I/O-bound HTTP work specifically.