Choosing a Ruby HTTP Client: Butler vs. Faraday vs. Excon vs. HTTParty vs. Net::HTTP

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.

Net::HTTP

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

The ecosystem play — 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.

Excon

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.

HTTP.rb

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.

HTTParty

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

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

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.

Side-by-side

CapabilityButlerFaradayExconNet::HTTP
Fiber-native concurrencylimited
HTTP/2 (ALPN, multiplexed)✓ (first-class)adapter-dependentlimiteddepends
Connection poolingadaptermanual
Retries + backoff/jitterbuilt-inmiddlewarelimitedmanual
Circuit breakerbuilt-inexternal gemexternal gemexternal gem
Deadlines (total budget across retries)
Native request stubbingvia WebMockvia WebMockvia WebMock

This describes each library's design intent, not an independently re-verified benchmark of every listed library's current release — re-check before quoting it externally. Full matrix (including HTTParty, HTTP.rb, and Async::HTTP) and a real Sequential/Concurrent benchmark run against several of these clients in the README.

Further reading