Perl MVC,
compiled
at boot.

it’s alive

Routes, guards, handlers and mounts are resolved once at to_app and frozen. Nothing is interpreted per request: dispatch is a hash lookup and your handler is a plain coderef call, about two microseconds after the request arrived.

package MyApp;
use Punk;

get  '/'          => sub { $_[0]->text('oi!') };
get  '/books/:id' => 'Web::Book#view';
post '/books'     => 'Web::Book#create';

websocket '/chat'   => 'WS::Chat#join';
sse       '/events' => 'Live#feed';

1;

The numbers

requests / second, basic hello world app
200,000+
per request, routed and answered
1.9µs
Punk’s own share of that is around
0.5µs
of the bare PSGI app’s throughput
99%

The whole application - routing table, guard chains, controller resolution, template engines, database handles, OpenAPI validation - is built once, when to_app is called. What runs per request is C: an XS dispatcher walking frozen structures, calling your Perl exactly once, for the handler.

Measured with bench/dispatch.pl in the Punk distribution: the PSGI coderef called in a loop with a fresh environment, no socket and no server, each app in its own interpreter, best of nine rounds. A bare PSGI sub returning the same string costs 1.4µs on the same box, which is what the 0.5µs above is measured against. The requests/second figure is bench/bench.pl: the same apps hosted on two Hyperman workers and driven with wrk -t4 -c64, median of three runs, rounded - server and client share the box and it swings several percent between runs, which is why the bare app measures the same 200,000 and why the ratio is the honest half of that pair. All of it on one Apple M5, and yours will differ; the ratios are the part that travels.

Everything is a keyword

use Punk exports a DSL that reads like a routing table and compiles like a program. Each keyword below is resolved at boot; a typo'd controller croaks before the app serves a single request.

Async without the ceremony

Return a future from any handler and Punk awaits it. On a Hyperman worker the request parks on the event loop and the worker keeps serving; on any other PSGI server the same code blocks politely instead. No colored functions, no framework fork.

$c->promise, $c->timer($secs) and $c->await($f) are built in, in C, as Punk::Future.

# Punk::Future: on a Hyperman worker the loop keeps serving
# other requests while this one waits.
get '/report/:year' => sub {
    my ($c) = @_;
    $c->model('report')->search({ year => $c->param('year') })
      ->on_done(sub { $c->json($_[0]) });
};

# The async model backend: same contract as Punk::Model::DBI,
# same SQL, but every method returns a future.

The spec is the router

Point api at an OpenAPI 3.1 document and the mount is generated at boot: every operation dispatches to the controller method named by its operationId, request bodies and parameters are validated before your code runs, and the document’s security schemes become ordinary Punk guards.

Start from the other end with punk new MyApp --api ./openapi.json and the controllers are stubbed for you, one per tag.

# Mount an OpenAPI 3.1 document: each operation dispatches to
# the controller method named by its operationId, with request
# validation and security-as-guards resolved at boot.
my $api = api 'openapi.json';

docs '/api-docs';    # and the generated reference UI
The generated OpenAPI reference UI for a mounted spec
The generated reference UI - docs '/api-docs'

A queue in the box

Punk::Queue is a job queue as a plugin: named queues, retries with backoff, cron, locks, broadcast - and its keywords (queue, task, cron) sit beside Punk’s own. Funky, its admin UI, mounts under a guard you control.

use Punk::Plugin::Queue;

plugin 'Queue' => {
    dsn   => 'dbi:Pg:dbname=myapp',
    admin => { prefix => '/queue', guard => 'Web::Auth#admin' },
};

task 'mail.send' => 'Job::Mail#send';
cron '0 3 * * *' => 'Reports#nightly';

post '/signup' => sub {
    my ($c) = @_;
    $c->json({ job => $c->enqueue('mail.send' => [ $c->param('to') ]) });
};
Funky queue overview: stats bar, state breakdown by queue and task, 24-hour throughput chart
Funky - the queue overview, dark theme
Funky jobs table with the filter toolbar and bulk actions
Jobs - filters, search, bulk retry
Funky job detail: arguments, result, attempts, retry and remove actions
One job - args, result, retries

The command line

punk new writes a running application - routes, a controller, views, config, tests. Then punk dev serves it with restart-on-change, punk routes prints the compiled table, punk doctor reports the environment and C ABIs, and punk config check resolves every secret before production does.

Terminal output of punk routes: the compiled routing table
punk routes
Terminal output of punk doctor: versions, C ABIs, application health
punk doctor
The welcome page a fresh punk-new application serves
What punk new boots into

Built on its own stack

Punk is the web tier of a set of zero-dependency Perl+C distributions that speak to each other through public C ABIs - no glue code, no foreign function overhead.

Sixty seconds

cpanm Punk
punk new MyApp
cd MyApp && punk dev

Three commands to a running, tested application. Then read Getting started, or see what a finished one looks like - a live chat over WebSockets, with its own docs site and OpenAPI reference, ships in Punk’s example/ directory.

The Chat example application: a live WebSocket chat room built with Punk
Built with Punk - the Chat example