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.

- 2.01µs on a dynamic route with a capture - dynamic dispatch at static speed

- +0.3µs for a fifty-route table over a one-route table - table size is nearly free

- 2.06µs per Punk::Model::DBI get - the statement is memoised on the connection

- 3.2µs through a mounted OpenAPI operation, request validation included

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.

-

Routing & guardsgetpostputpatchdelany, with:nameand*restcaptures.underopens a nestable guard scope - return a response to short-circuit, nothing to continue.

-

WebSocketswebsocket '/chat' => 'WS::Chat#join'- routed like a GET, guarded like any route, then handed a live connection on the server’s event loop. Rooms broadcast; subprotocols negotiate.

-

Server-Sent Eventssse '/events' => 'Live#feed'- the lighter sibling: an EventSource stream with heartbeats and retry control, fully non-blocking on a Hyperman worker.

-

Sessions, CSRF, CORS, headersHMAC-SHA256 signed cookie sessions - or server-side on any cache store with store =>, where logout genuinely revokes andsession_rotateshuts fixation; single-use CSRF tokens that spend on use; CORS and theheaderspolicy answered inside the dispatcher, so the headers reach the 404s too.

-

Authenticationauthwires a user model to the session:auth_guard(role => 'admin')on any scope,$c->current_usermemoized, PBKDF2 passwords and single-use email tokens. The signed-in check runs entirely in C, and a TOTP second factor is one plugin away.

-

OAuth2, all three sidesoauth2logs your users in with Google, GitHub or any OIDC provider; checkers guard your API's Bearer tokens; andoauth2_serveris a full authorization server, signed by Crypt::JWS.

-

Downloads & negotiation$c->send_file- ETags, 304s, byte ranges and HEAD handled, never more than 64KB of file in memory.$c->respond_toanswers theAcceptheader with the right format,Varyincluded.

-

Uploads that streamA large multipart part spills to a temp file as it arrives - a 128MB upload costs a worker 15.5MB, not 275. blob_putstores it by contents so a filename never becomes a path, and the ClamAV plugin scans it by descriptor before you keep it.

-

OpenAPI 3.1api 'openapi.json'mounts the document: operations dispatch byoperationId, requests validate against the schema, security schemes become guards.docsserves the reference UI.

-

Config & secretsLayered YAML ( punk.yml→ env → local). Secrets are references -{ $env: KEY },{ $file: path },{ $exec: cmd }- resolved at boot and redacted everywhere else.

-

Models, sync or asyncOne six-method contract - get search all create update delete.Punk::Model::DBIblocks;Punk::Model::DBIx::Loopreturns futures and runs on the worker’s loop. Same SQL, same shapes.

-

Outbound HTTP$c->uais a Fetch agent - HTTP/2, futures, a keep-alive pool shared per worker. Named agents with their own timeouts and jars:ua partner => { timeout => 2 }.

-

Mountsstaticfor files,markdownfor a docs site rendered and frozen at boot (this one),mountfor any PSGI app under a prefix. Longest prefix wins.

-

GraphQLgraphql '/gql' => 'schema.graphql'- compiled once at boot onto GraphQL::Houtou’s XS VM, resolvers asController#methodtargets, GraphiQL on tap.

-

Caching & idempotencycachewithcompute, TTL and named stores; a per-worker memory tier kept coherent over Hyperman’s bus; andIdempotency-Keyreplay on unsafe methods, so a retriedPOST /ordersis one order.

-

Rate limitingrate_limitcounts in Hyperman’s shared arena - exact across the pool, not per worker - and answers 429 with the headers.$c->block_ipdrops an abuser ataccept, before a byte is read. Fails open.

-

ObservabilityRequest ids on every response and log line; a Prometheus /metricswhose labels are the compiled route table, so cardinality cannot run away;/healthzand/readyzthat mean different things; OpenTelemetry through C observer tables - and a backend to send it to.

-

SEO from the route tableThe sitemap is the compiled routes with a filter over it - guarded pages excluded with nobody maintaining a list. $c->assetfingerprints URLs by contents, and ConditionalGet answers 304 without running the handler.

-

I18nJSON catalogues parsed once at boot, negotiated per request with real subtag fallback, CLDR plural rules, and escaping that trusts the catalogue but never the substitutions. Templates read directly.

-

Extensible by keywordplugin,hook,middleware,helper- and a plugin can install keywords of its own withinstall_kw, indistinguishable from the core DSL. Nine plugins ship in the box, four more as their own dists.

-

An all-C coreForty-seven XS units behind the DSL: router, dispatcher, request, context, sessions, SSE frames, WebSocket codec. Speaks Hyperman and DBIx::Loop natively through their C ABIs.

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

Not just the web tier

Two of the pieces around Punk are large enough to have their own page.

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.

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.

- Hyperman an event-loop PSGI server

- Fetch HTTP/2 Future-based user agent

- DBIx::Loop non-blocking DBI on your event loop

- Open::API OpenAPI 3.1 server and client

- Template::Stencil a fast template engine

- File::Raw::JSON fast JSON for File::Raw

- GraphQL::Houtou an XS GraphQL parser and runtime

- Crypt::JWS JWS signatures on libcrypto

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.