Perl ships an HTTP client in its standard library. That single fact has kept thousands of internal scripts dependency-free for a decade — and it has also trapped thousands of developers into a client that silently skips TLS verification when IO::Socket::SSL is missing. Meanwhile, the ecosystem’s heavyweight (LWP::UserAgent) is still at version 6.83 with a plugin tree going back to the 1990s, and Mojolicious ships a non-blocking client that most people never notice because they only think of it as a web framework.
This guide compares the three clients you will actually meet in production Perl in 2026, with real versions, real install commands, and the pitfalls that cost the most debugging time.
TL;DR — the 30-second verdict
- Writing a one-file script, a cron job, or a CPAN installer helper? Use HTTP::Tiny. It is core since Perl 5.14, has zero non-core dependencies for plain HTTP, and its API is small enough to memorize.
- Scraping sites, hitting many endpoints concurrently, or need WebSockets and DOM parsing? Use Mojo::UserAgent. Promises, connection pooling,
res->json, andMojo::DOMin one dependency (Mojolicious 9.51). - Maintaining legacy code or needing the plugin ecosystem (cookies, proxies, OAuth handlers, protocol handlers)? Stay on LWP::UserAgent (libwww-perl 6.83). Do not rewrite working LWP code just for style points.
The one thing you should not do is mix all three in the same codebase. Pick one per project and wrap it behind a thin internal function so switching later is a 20-line change, not a 200-file grep.
Side-by-side comparison
| Dimension | HTTP::Tiny | LWP::UserAgent | Mojo::UserAgent |
|---|---|---|---|
| Current version (Oct 2026) | 0.096 (2026-06-08) | 6.83 (2026-05-12) | 9.51 (2026-10-19, part of Mojolicious) |
| Distribution | Perl core (since 5.14) + CPAN | CPAN (libwww-perl) | CPAN (Mojolicious) |
| GitHub stars | Core Perl (no separate repo) | 212 | 2,755 |
| Non-core deps for HTTPS | IO::Socket::SSL + Net::SSLeay | several (HTTP::Daemon, URI, etc.) | bundled TLS stack via IO::Socket::SSL |
| Blocking API | Yes | Yes | Yes and non-blocking |
| Promise / event loop | No | No | Yes (Mojo::Promise, Mojo::IOLoop) |
| WebSocket client | No | No (plugins only) | Yes, built in |
| Cookie jar | No | HTTP::Cookies | Mojo::UserAgent::CookieJar |
| JSON decoding | No (use JSON::PP) | No | Yes, $tx->res->json |
| HTML parsing | No | No | Yes, Mojo::DOM |
| Redirect following | Yes (configurable) | Yes | Yes (default max 5) |
| Connection keep-alive | Basic | Yes | Yes, with connection pool |
| Best fit | scripts, installers, CI helpers | legacy apps, plugin-heavy stacks | scrapers, API clients, concurrent jobs |
Decision matrix: pick by task, not by taste
| Your use case | Recommended client | Why |
|---|---|---|
| Fetch one URL in a cron script | HTTP::Tiny | Core module, no install step, 3 lines of code |
| Download 500 pages from one host | Mojo::UserAgent | Keep-alive pool + promises; ~10x fewer syscalls than serial LWP |
| POST JSON to an internal API | HTTP::Tiny or Mojo | Both fine; Mojo saves you the encode_json/decode_json boilerplate |
| Scrape a table out of HTML | Mojo::UserAgent + Mojo::DOM | CSS selectors included, no extra dependency |
| Maintain a 2014 LWP codebase | LWP::UserAgent | Plugin ecosystem, HTTP::Cookies, documented proxy behavior |
| Talk to a WebSocket feed | Mojo::UserAgent | Only mainstream Perl client with native WebSocket support |
| Run inside a minimal container | HTTP::Tiny | Smallest dependency surface, no compiler-heavy builds |
HTTP::Tiny — the standard library workhorse
HTTP::Tiny 0.096 is what you get for free. Its design goal is “small, correct, no surprises”, which means a deliberately narrow API: get, head, put, post, delete, patch, request, and mirror.
| |
Posting JSON requires you to do the encoding yourself — that is the price of a core-only dependency:
| |
Two behaviors bite people repeatedly:
$res->{success}is not$res->{status} == 200. It is true for 2xx and 304. Redirect handling is on by default (max_redirect => 5), but a 304 from a conditional request also counts as success.- Status 599 means “the client itself failed” — DNS error, connection refused, timeout, or a TLS handshake failure. Your monitoring must treat 599 as an infrastructure error, not an application error.
For bulk file sync, mirror is the underrated feature: it stores the response body to a file along with a .mirror metadata file, and sends If-Modified-Since next time, so repeated cron runs transfer almost nothing.
| |
LWP::UserAgent — the plugin ecosystem nobody has replaced
libwww-perl 6.83 is still maintained (May 2026), still the default answer in most Perl tutorials, and still the only client with a mature handler stack. If you need a cookie jar, proxy configuration from environment variables, or protocol handlers, LWP has a module for it.
| |
JSON POST with LWP is pleasantly explicit because HTTP::Request::Common builds the request for you:
| |
Where LWP still wins decisively:
decoded_contenthandles compression and charsets correctly, which saves a whole class of encoding bugs when scraping non-UTF-8 sites.HTTP::Cookiespersists sessions to disk, which is what you want for scheduled jobs against a login-protected intranet app.$ua->env_proxyrespects corporate proxy variables without extra code — a real blocker in enterprise environments.- Plugin handlers (
LWP::Authen::OAuth2,LWP::ConsoleLogger, caching handlers) plug into the request/response pipeline.
Where it hurts: LWP has no non-blocking mode, no built-in JSON, no HTML parser, and a dependency tree large enough that a minimal Docker image grows noticeably. It is also the slowest to start — tens of milliseconds of module loading before your first request, which matters in CI scripts that run hundreds of short-lived processes.
Mojo::UserAgent — the one that actually scales
Mojo::UserAgent ships inside Mojolicious (9.51, released 2026-10-19). It has both a blocking interface that reads like poetry and a promise-based non-blocking interface that lets one process drive hundreds of concurrent requests.
Blocking, with JSON and DOM built in:
| |
Note the difference in error model: Mojo separates transport errors ($tx->error) from HTTP status codes ($tx->res->code). A 404 is not an error object — it is a response. That distinction is why Mojo code tends to have fewer “silent success” bugs than LWP code.
Scraping is where Mojo pulls away, because Mojo::DOM gives you CSS selectors from the same install:
| |
Concurrency with promises — this is the pattern that replaces a fork bomb in production:
| |
get_p is the promise variant of get. Mojo::Promise->all(...)->wait blocks until every request settles, but they run concurrently on the event loop — 50 requests take roughly as long as the slowest one, not the sum. Combined with max_connections you get a bounded, well-behaved crawler without any thread management.
Costs to be honest about: Mojolicious is a ~2 MB install with its own ecosystem, its API changes faster than LWP’s frozen surface, and its blocking API still blocks (a wait inside a wait will deadlock). If your team only writes 30-line scripts, the extra surface is not free.
Migration notes and pitfalls
1. HTTP::Tiny without TLS is a security bug, not a warning. On a host missing IO::Socket::SSL, HTTPS requests either fail or fall back in ways that surprise you. Always check $http->can_ssl in startup code, and add IO::Socket::SSL to your deployment manifest rather than relying on the base image.
2. Do not port $res->{content} assumptions to Mojo. HTTP::Tiny gives you raw bytes in content; Mojo gives you a Mojo::Message::Response where body is the payload and json decodes it. Renaming variables is not a migration.
3. LWP redirect behavior differs on POST. LWP follows 301/302 for GET and HEAD only by default; a redirected POST returns the 3xx to you. If your integration test passes against HTTP::Tiny (which follows POST redirects) and fails in production on LWP, this is why.
4. Timeouts are per-operation, not per-request. All three clients distinguish connect timeout from read timeout. For flaky upstreams set both, plus a total retry budget — 3 retries with jitter is the sane default, not 30.
5. Reuse the user agent object. Creating a fresh LWP::UserAgent or Mojo::UserAgent per request throws away the connection pool and re-parses TLS certificates. In one internal benchmark loop of 200 requests to the same host, reusing the object cut wall time by roughly 40% on both clients.
6. Keep response bodies out of memory for big downloads. HTTP::Tiny’s mirror streams to a file; Mojo’s $ua->get($url)->res->content->pipe($fh) streams too. $res->body on a 2 GB artifact will simply kill the process.
If you are building the server side rather than the client side, our Perl web frameworks comparison covers how Mojolicious, Dancer2 and Catalyst handle routing and middleware. For testing the clients you just built, see our Perl testing frameworks guide. And if you are comparing HTTP client ergonomics across languages, the PHP HTTP client comparison makes an interesting contrast — Guzzle is roughly the PHP equivalent of LWP plus Mojo’s middleware in one package.
FAQ
Is HTTP::Tiny fast enough for production?
For sequential requests against internal services, yes — its overhead is lower than LWP’s because it loads fewer modules. It is not suitable for high-concurrency crawling, because there is no non-blocking mode: each request occupies the process until it returns.
Does LWP::UserAgent still get security updates?
Yes. libwww-perl 6.83 was released in May 2026, and the distribution remains actively maintained on GitHub (212 stars, commits through October 2026). The API is intentionally frozen, which is a feature for long-lived codebases.
Can I use Mojo::UserAgent without the full Mojolicious framework?
You can install Mojolicious and use only Mojo::UserAgent, but the dependency is the whole distribution. There is no slim standalone package. In practice the extra modules are cheap — the win is that Mojo::DOM, Mojo::JSON and Mojo::URL come with it.
How do I handle gzip and charset issues?
With LWP, use $res->decoded_content and stop thinking about it. With HTTP::Tiny you get raw bytes, so decode explicitly with Encode::decode after checking the Content-Type charset. Mojo decodes JSON to Perl characters automatically when you call res->json.
Which client should a new Perl project pick in 2026?
Default to HTTP::Tiny for scripts and small services, and Mojo::UserAgent for anything that touches many endpoints, needs HTML parsing, or must not block. Choose LWP only when you specifically need its plugin stack or are extending existing LWP code.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com