NAME
Hyperman - an event-loop PSGI server
SYNOPSIS
use Hyperman;
my $app = sub { [ 200, [ 'Content-Type' => 'text/plain' ], [ 'Hello' ] ] };
Hyperman->run( app => $app, port => 8080, workers => 4 );
# async: a handler may return a Hyperman::Future of the response
my $async = sub {
my $env = shift;
Hyperman->timer(0.5)->then(sub {
[ 200, [ 'Content-Type' => 'text/plain' ], [ 'later' ] ];
});
};
DESCRIPTION
Hyperman is a PSGI server built on a prefork supervisor with a per-worker XS event loop (Hyperman::Loop over a pluggable readiness backend), aiming to match a JIT HTTP server's throughput without a JIT. Handlers may return a Hyperman::Future (or any Future-compatible object) of the PSGI response; the connection is parked while the worker keeps serving, and awaiting a future inside a handler pumps the worker's own loop. Runs any Plack app via plackup -s Hyperman.
The entire implementation is XS: the loop, HTTP machinery, Futures, and the process model live in C (include/hyperman/), with per-package XS interfaces (xs/).
run
Hyperman->run(
app => $psgi_app, # required
host => '0.0.0.0',
port => 8080, # a scalar, or an arrayref of ports
# ([80, 8080]) for several plain
# listeners sharing these options
workers => 0, # 0/unset = one per CPU; 1 = in-process
# dev mode (no supervisor)
idle_timeout => 60, # close idle keep-alive conns (secs)
header_timeout => 30, # slow/partial request guard (secs)
max_pipeline => 32, # requests per conn per wakeup
reuseport => 0, # per-worker listeners (SO_REUSEPORT;
# Linux accept scaling - regressed on
# macOS, keep off there)
access_log => '/var/log/app.log', # fast built-in Combined-log
# writer (a path, or an open handle
# like \*STDERR). Or pass a coderef
# for custom logging:
# sub { my ($env,$status,$bytes)=@_ }
# ($bytes is undef for streaming)
max_requests_per_worker => 0, # recycle a worker after N requests
shutdown_grace => 30, # bound on graceful drain (secs)
affinity => 0, # pin worker i to core i%ncpu (Linux)
http2 => 0, # accept HTTP/2 (h2c, and h2 over TLS via
# ALPN); needs the nghttp2 build
tls_cert => $cert_pem, # serve HTTPS (needs the OpenSSL build,
tls_key => $key_pem, # Hyperman->has_tls); both required
tls_ca => $ca_pem, # verify client certs against this CA
tls_verify => 'require', # none (default) | optional | require
tls_sni => { # per-hostname certificates (SNI)
'other.example' => { cert => $c2, key => $k2 },
},
);
The event backend is chosen automatically (kqueue, io_uring, epoll, then poll) and can be forced with HYPERMAN_BACKEND.
Multiple listeners
A single run can bind several listeners, each independently plain or TLS - the common case being plain :80 beside HTTPS :443. Pass listen an arrayref of per-listener hashrefs; each takes its own port (required) plus any of host, tls_cert, tls_key, tls_ca, tls_verify, tls_sni, http2, and redirect_https. A missing per-listener field falls back to the top-level value of the same name, and the top-level port/listen are mutually exclusive shorthands - port => [80, 8080] is sugar for several plain listeners sharing the top-level options.
Hyperman->run(
app => $app,
workers => 8,
listen => [
{ port => 80, redirect_https => 443 }, # 301 every request to https
{ port => 443, tls_cert => $cert, tls_key => $key },
],
);
redirect_https makes a listener answer every request with a 301 to the same host and request-target on the given https port (in C, before the app is reached); the value is that target port, and a bare true value means the standard 443. Its SERVER_PORT and, for TLS listeners, psgi.url_scheme reflect the listener the request arrived on. All listeners serve the one app. Ports below 1024 (80, 443) still require the process to start as root or hold CAP_NET_BIND_SERVICE.
access_log
When access_log is a path or an open filehandle, Hyperman formats an Apache-style Combined Log Format line
host - user [dd/Mon/YYYY:HH:MM:SS +ZZZZ] "METHOD URI PROTO" status bytes "referer" "ua"
entirely in C and appends it to the file, with no per-request Perl call on the hot path. The timestamp is cached and recomputed at most once a second, lines are buffered and flushed once per event-loop wakeup, and a file target is opened once in the parent and shared O_APPEND across workers so their lines stay interleaved without tearing. Quoted fields are escaped, so a hostile URI or User-Agent cannot forge a log line. REMOTE_ADDR is captured at accept and is also visible to the app in $env.
Pass a coderef instead (sub { my ($env, $status, $bytes) = @_ }) for full control; that path calls back into Perl for every response.
HTTP/2
With http2 => 1 (requires the nghttp2 build; Hyperman->has_http2 reports it), a connection whose first bytes are the HTTP/2 preface is served as cleartext HTTP/2 (h2c, prior knowledge) via nghttp2: multiplexed streams, HPACK, and flow control, with each stream dispatched to the app like any request (sync, Hyperman::Future, or psgi.streaming responses all work). HTTP/1.1 remains the default on the same port.
TLS / HTTPS
With tls_cert/tls_key (PEM paths; requires the OpenSSL build, Hyperman->has_tls), the listener serves HTTPS: a non-blocking TLS handshake is driven on the event loop, then reads/writes go through OpenSSL. The certificate and key are validated once before forking, so a bad pair fails fast. Combined with http2 => 1, ALPN negotiates h2 vs http/1.1 per connection on the same port - the path browsers use for HTTP/2. psgi.url_scheme is https for TLS requests, and HTTPS is set to on.
Client certificates (mTLS). tls_verify (optional/require, with tls_ca as the trust anchor) requests and checks a client certificate; require fails the handshake without a valid one. The result is surfaced in $env the mod_ssl way: SSL_CLIENT_VERIFY (SUCCESS/FAILED/NONE), SSL_CLIENT_S_DN, SSL_CLIENT_I_DN, plus SSL_PROTOCOL/SSL_CIPHER.
SNI (multiple certificates). tls_sni maps hostnames to their own cert/key; the certificate is chosen from the TLS ServerName, falling back to tls_cert/tls_key.
h2c Upgrade. With http2 => 1 over cleartext, an HTTP/1.1 request carrying Upgrade: h2c is answered with 101 Switching Protocols and the connection continues as HTTP/2 (the original request becomes stream 1) - as well as the prior-knowledge preface. Over TLS, h2 is chosen by ALPN instead.
With workers > 1 a supervisor process manages the pool: a crashed worker is respawned (exponential backoff on crash loops; clean exits respawn immediately), SIGHUP or SIGUSR2 recycles all workers with zero downtime (new workers start, old ones drain gracefully), SIGUSR1 makes every worker dump its stats to stderr, and SIGTERM / SIGINT drain - bounded by shutdown_grace - and exit. With workers => 1 the server runs in the calling process, no supervisor.
timer / io_ready
my $f = Hyperman->timer($secs); # Future, resolves after $secs
my $g = Hyperman->io_ready($fh, 'r'); # Future, resolves on readiness
Both require a running worker loop. Hyperman->loop returns the current worker's Hyperman::Loop (or undef outside one).
stats
my $s = Hyperman->stats; # in-app, inside a worker
# { requests => N, accepts => N, bytes_out => N, connections => N,
# backend => 'kqueue', pid => $$ }
Per-worker counters; returns undef outside a running loop.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under the Artistic License 2.0.