Fiber-native concurrency, HTTP/1.1 + HTTP/2, and production-grade resilience in one Ruby HTTP client — without ever exposing its transport in the public API.
One client. Modern concurrency. Production-ready HTTP. Butler is the modern Ruby
HTTP client built for concurrent, resilient applications — it 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/1.1 + HTTP/2 today. HTTP/3 architecture-ready. Early internal QUIC
packet-protection groundwork already exists (RFC 9000/9001-vector-verified), but it's not a usable
transport yet — no http_version: :http3 today, on purpose, until it actually is one.
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 |
Faraday gives you an ecosystem. Butler gives you a modern HTTP runtime.
Async::HTTP gives you the foundation. Butler gives you the application-facing client.
That's 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.
Competitor versions checked 2026-09-02: faraday 2.14.3,
excon 1.7.1. Capability cells describe design intent, not an independent re-test of
every release — re-check before quoting externally.
Fiber::Scheduler actually is, thread-per-request vs. Fiber-per-request, and
why it fits I/O-bound HTTP work specifically.