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.

fetch.pl · core only HTTP::Tinyno CPAN
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.

Verify before you trust 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.

crawl.pl · Mojolicious Mojo::UserAgentnon-blocking
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.

Decision order

Stop at the first yes
Three ordered questions and the client each one selects.
#QuestionIf yesWhy
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

  1. Checking the status but not the transport. A DNS failure is not a 500. Test the success flag, then branch on status.
  2. No timeout. The default is generous or absent depending on the client. A hung request in a cron job is a silent outage.
  3. Following redirects blindly into a different host. If the request carries credentials, cap max_redirects and check the final URL.
  4. 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.

Examples are not CI-tested The code above is written to be read and was not executed by an automated harness for this build. 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.

Public correction trail Prefer a public record? Open a sourced issue in the PerlCoders repository.