Guide · core modules and tooling
Making HTTP requests in Perl in 2026
HTTP::Tiny is in core and does more than people expect. Mojo::UserAgent earns its dependency when you need concurrency or a DOM. Here is where the line falls — and the one case where the honest answer is “either”.
Almost every Perl script that touches the network makes one of three choices, and most people make it by copying whatever the last script in the repository did. The three are worth distinguishing, because the difference is not capability — it is what you are allowed to install and whether more than one request is in flight at a time.
Start from core
HTTP::Tiny has shipped in the standard distribution since Perl 5.14. On a host where
you cannot run cpanm, it is not the compromise option — it is the option. It does
GET, POST, form encoding, redirects, timeouts, proxies from the environment, and TLS when
IO::Socket::SSL and Net::SSLeay are present.
use strict;
use warnings;
use HTTP::Tiny;
my $http = HTTP::Tiny->new(
agent => 'example/1.0 ',
timeout => 15,
);
my $res = $http->get('https://example.org/api/status');
die "request failed: $res->{status} $res->{reason}\n"
unless $res->{success};
print $res->{content};
Two details people miss. The trailing space in the agent string is deliberate: HTTP::Tiny
appends its own identifier after it, so you end up with example/1.0 HTTP-Tiny/…
rather than a run-together token. And $res->{success} is false for a connection
failure as well as for a 4xx or 5xx — status 599 with the error in
content. Checking only status == 200 hides transport failures.
HTTP::Tiny only verifies TLS certificates when verify_SSL is enabled and
IO::Socket::SSL is available. Check the behaviour of the exact version on your host
rather than assuming; this is precisely the kind of default that has changed across releases.
When to add a real user agent
Mojo::UserAgent is worth the dependency when at least one of three things is true:
you need many requests in flight, you need to select inside the response body, or you are already
running Mojolicious and the event loop exists anyway.
use Mojo::UserAgent;
use Mojo::Promise;
my $ua = Mojo::UserAgent->new(max_redirects => 5, connect_timeout => 10);
my @urls = qw(https://example.org/a https://example.org/b);
Mojo::Promise->all(
map { $ua->get_p($_) } @urls
)->then(sub {
for my $tx (map { $_->[0] } @_) {
my $title = $tx->result->dom->at('title');
print $title ? $title->text . "\n" : "(no title)\n";
}
})->catch(sub { warn "failed: $_[0]\n" })->wait;
The DOM selector on line 12 is the part that is genuinely hard to replicate with core modules. Regular expressions over HTML work until the markup changes shape; a CSS selector against a parsed document does not have that failure mode.
Where LWP still fits
LWP::UserAgent is not deprecated and not a mistake. It carries the largest surface
of protocol handlers, authentication schemes and cookie behaviour, and a great deal of working
code already uses it. The reason it is not the default recommendation here is dependency weight,
not quality: it pulls a substantial tree, which matters exactly when the host is locked down —
the same situation that argues for HTTP::Tiny.
Deciding
Three questions, in order. The first one that gets a yes ends the decision.
| # | Question | If yes | Why |
|---|---|---|---|
| 1 | Can you install anything on the host? | No → HTTP::Tiny |
It is already there. Nothing else in this table is. |
| 2 | Do you need concurrency, or to select inside the response body? | Yes → Mojo::UserAgent |
Promises and a real DOM, in one dependency with no non-core requirements. |
| 3 | Do you need an unusual auth scheme, protocol handler or cookie behaviour? | Yes → LWP::UserAgent |
The widest protocol surface of the three, and the most existing code to copy from. |
| — | None of the above | Either core or Mojo | This is the honest answer for most one-off scripts. Pick the one your team already reads. |
Four mistakes that survive every rewrite
- Checking the status but not the transport. A DNS failure is not a 500. Test the success flag, then branch on status.
- No timeout. The default is generous or absent depending on the client. A hung request in a cron job is a silent outage.
- Following redirects blindly into a different host. If the request carries credentials, cap
max_redirectsand check the final URL. - Parsing HTML with a regular expression. Acceptable for a throwaway; a maintenance liability the moment the script runs twice.
Versions and testing
This guide is written against Perl 5.14 or later for HTTP::Tiny availability, and
Mojolicious 9.x for the promise API shown above. The current stable Perl release is a
typed slot on the Now page awaiting editor verification — this guide does
not assert a version it has not checked.
codeTested is false for this article and the badge stays until a real
run exists. If an example is wrong, that is a correction we want.
Primary documentation
Module behaviour and defaults change. Check the documentation for the version installed on the target host; these links are the maintained references behind the capability claims in this guide.
- HTTP::Tiny documentation on MetaCPAN
- Mojo::UserAgent documentation
- LWP::UserAgent documentation on MetaCPAN