What We Learned Shipping PAGI
PAGI (Perl Asynchronous Gateway Interface) is a new web specification and reference server for Perl, designed to bring first-class async/await support to web development. Think of it as Perl's answer to Python's ASGI - a modern foundation for WebSocket, Server-Sent Events, and HTTP applications using Future and Future::AsyncAwait syntax.
Since the first stable release on December 24th, we've shipped seven releases in four days. This pace wasn't planned - it emerged from squashing bugs reported by CPAN Testers, especially on important but less common platforms like FreeBSD, along with rapid iteration on the API. Here's what we learned along the way.
Sometimes the Right Code is No Code
One of the more interesting decisions we made was removing a feature: built-in sendfile support.
The initial implementation used IO::Async::FileStream with Sys::Sendfile to efficiently stream large files directly from disk to socket, bypassing userspace copying. It worked beautifully on Linux. Then we tested on FreeBSD.
The problem wasn't just FreeBSD - it was the interaction between sendfile and non-blocking sockets, edge cases with partial writes, and the complexity of handling all the ways different operating systems implement (or don't implement) zero-copy file transfers. We found ourselves writing increasingly elaborate platform-specific workarounds.
We stepped back and asked: what problem are we actually solving? In production, large file serving should go through a reverse proxy anyway. Nginx's X-Sendfile (or X-Accel-Redirect) and Apache's mod_xsendfile exist precisely for this use case. They're battle-tested, optimized, and someone else maintains them.
So we removed sendfile entirely and created PAGI::Middleware::XSendfile instead. Your PAGI application returns a special header, and your reverse proxy handles the actual file transfer. The Unix philosophy wins again: do one thing well, and compose with specialized tools.
That said, this isn't ideological. If someone wants to contribute a robust, cross-platform sendfile implementation, we'd welcome the PR. The current solution is pragmatic, not dogmatic.
Async Error Handling Done Right
Asynchronous programming has a dirty secret: errors love to disappear.
In synchronous code, an exception bubbles up naturally. In async code with fire-and-forget Futures, exceptions can vanish into the void. Your background task fails, nobody notices, and you spend hours wondering why something silently stopped working.
PAGI originally used a pattern called retain() to keep Futures alive:
$self->retain($self->some_async_operation());
This worked for keeping the Future from being garbage collected, but it had a problem: if some_async_operation() failed, the error was quietly swallowed. The Future failed, nobody was listening, and life went on - except for the bug you didn't know about.
We replaced this with adopt_future():
$self->adopt_future($self->some_async_operation());
The difference is crucial. When an adopted Future fails, the error propagates to the parent context and gets logged. You see it. You can debug it. The failure is observable.
This applies throughout PAGI's internals - connection handling, protocol parsing, worker lifecycle management. Errors that previously might have disappeared now surface properly. It's a small API change that dramatically improves debuggability.
For application developers, the pattern extends to your own code. When you spawn background work in a request handler, adopt it rather than retaining it. Your future self debugging a production issue will thank you.
Making PAGI Feel Like Perl
One principle guided several of our developer experience decisions: PAGI should feel like Perl, not like a foreign framework that happens to run on Perl.
The clearest example is the new -e and -M flags for pagi-server:
# Inline app, just like perl -e
pagi-server -e 'sub { my ($scope, $receive, $send) = @_; ... }'
# Load modules first, like perl -M
pagi-server -MPAGI::App::File -e 'PAGI::App::File->new(root => ".")->to_app'
If you've used perl -e for quick scripts, this syntax is immediately familiar. No ceremony, no boilerplate files for simple cases.
This philosophy extends to other areas:
Test::Client now traps exceptions by default, matching how Perl developers expect test failures to behave. Multi-value headers, query parameters, and form fields work naturally.
Environment modes follow the pattern established by Plack: -E development enables debugging middleware, -E production optimizes for performance. The detection is automatic based on TTY.
Signal handling follows Unix conventions. SIGTERM and SIGINT trigger graceful shutdown. SIGHUP reloads workers. SIGTTIN/SIGTTOU adjust worker counts. It behaves like the system tools Perl developers already know.
The goal is reducing cognitive overhead. You shouldn't need to learn "the PAGI way" for things Perl and Unix already have conventions for.
What's Next
PAGI is stabilizing, but there's plenty of interesting work ahead.
Documentation has been a major focus. We split the original Tutorial into two documents: a Tutorial for getting started with core concepts, and a Cookbook for advanced patterns like background tasks, database connection pooling, JWT authentication, and Redis-backed sessions. The goal is progressive disclosure - you shouldn't need to read about WebSocket heartbeat strategies before you've served your first HTTP response.
The middleware collection continues to grow. We have 37 middleware components now (authentication, rate limiting, CORS, sessions, and more), but there's always room for more.
We're considering a repository split and would love community feedback. Currently PAGI ships everything in one distribution: core spec, reference server, 37 middleware components, 19 apps, and test utilities. This is convenient (cpanm PAGI gets you everything) but means installing the full stack even if you only need parts.
Options we're weighing:
- Keep unified (current) - simpler while the spec is still evolving
- Split server - separate PAGI::Server as "reference implementation," enabling alternative servers
- Three-way split - core spec, server, and contrib (middleware/apps) as separate distributions
What would serve you better? Lighter installs? Easier contribution workflow? Alternative server implementations? We'd appreciate hearing your use case.
Most importantly, we're looking for contributors. The codebase is approachable - modern Perl with async/await, comprehensive tests, and documented internals. Whether you want to tackle something meaty like cross-platform sendfile, or something focused like a new middleware component, there's room for your ideas.
Get Involved
- GitHub: github.com/jjn1056/pagi
- CPAN: PAGI on MetaCPAN
- CONTRIBUTING.md: Guidelines for contributors, including our approach to AI-assisted development
- SECURITY.md: How to report vulnerabilities
PAGI is licensed under the Artistic License 2.0. It's a volunteer project - no timeline commitments, but serious about quality. If your organization needs priority support or custom development, contract work is available.
Async Perl web development is here. Let's build it together.
Top comments (2)
Big appreciation! and note that the Cookbook has
->retain()in many examples, but only mentions->adopt_futurein a note below one example.Thanks, yeah that needs a lot of cleaning up. PRs welcomed :).