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/). Only Plack::Handler::Hyperman is pure Perl.

run

Hyperman->run(
    app            => $psgi_app,   # required
    host           => '0.0.0.0',
    port           => 8080,
    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     => sub {        # optional; called after each response
        my ($env, $status, $bytes) = @_;   # $bytes 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.

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.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 99:

Non-ASCII character seen before =encoding in '—'. Assuming UTF-8