Ruby's HTTP client landscape is a set of trade-offs, not a clear winner — here's what each one is actually strong at, and where it isn't.
The standard library client — always available, no dependency to add. Low-level by design: no HTTP/2, no retries, no timeouts unless you set them explicitly (and it's easy to forget one and ship a client with no read timeout at all). A reasonable choice when you're making one or two simple calls and don't want a dependency; a poor foundation to build a service's entire outbound HTTP layer on without wrapping it yourself first.
Faraday gives you an ecosystem. Butler gives you a modern HTTP runtime. Faraday's strength is a huge middleware library and pluggable adapters (Net::HTTP, Excon, Typhoeus, and others underneath the same API). That flexibility is also its cost: the adapter/middleware abstraction is a real layer of indirection, and Faraday's own behavior can shift depending on which adapter you've plugged in underneath it. Strong choice if you want that ecosystem's breadth and are fine configuring resilience yourself via middleware.
Performance-focused, with a long track record in production. Less opinionated about resilience/developer-experience out of the box than newer clients — you're more likely to be reaching for extra gems or writing your own retry/circuit-breaker logic around it.
A genuinely Ruby-friendly API (chainable, readable call sites). Its concurrency/transport model is different from a Fiber-scheduler-based one — worth checking directly against your own app's concurrency model (thread-per-request web server vs. a Fiber-scheduler-based one like Falcon) before assuming it composes the way you'd expect.
The simplest possible ClassName.get(url) call, and genuinely the fastest way to get
something working. The cost of that simplicity: no connection reuse across calls (each bare
.get opens fresh), no HTTP/2, and no built-in resilience — fine for a quick script or
prototype, a real gap for anything calling the same API repeatedly in production.
Async::HTTP gives you the foundation. Butler gives you the application-facing client.
Async::HTTP is an excellent async foundation — real Fiber-native HTTP/1.1 and HTTP/2, per-origin
connection pooling, stream multiplexing, all implemented well. What it doesn't give you is a
client-facing API or resilience layer above that foundation — you're building your own
Client/Request/Response abstraction, your own retry policy,
your own circuit breaker, on top of it yourself.
Butler's position: take the strongest idea from each without the downsides it usually comes
with — a small, deliberately-scoped public API (Client, Request,
Response, Headers) in front of real HTTP/1.1 and HTTP/2,
Fiber-native concurrency (built on the same reactor Async::HTTP provides, not a
competing implementation of it), and resilience/security/observability that don't require four
extra gems to assemble yourself. The trade-off is real too: Butler is younger and has a smaller
ecosystem than Faraday's — if you need Faraday's specific middleware library, or an adapter it
supports that Butler doesn't aim to, that's a legitimate reason to stay with it.
| 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) | ✓ | — | — | — |
| Native request stubbing | ✓ | via WebMock | via WebMock | via WebMock |
This describes each library's design intent, not an independently re-tested benchmark of every
listed library's current release — re-check before quoting it externally. Competitor versions
checked 2026-09-02: faraday 2.14.3, excon 1.7.1,
httparty 0.24.2, http.rb 6.0.4. Full matrix and a real
Sequential/Concurrent benchmark run against several of these clients in the
README.