NAME

Punk::RateLimit - rate limiting and IP blocking over Hyperman's shared arena

SYNOPSIS

use Punk;

# a loose limit on everything, keyed by client IP
rate_limit limit => 300, window => 60;

# a tighter one on the API, keyed by an API-key header
rate_limit for => '/api', by => 'header:X-Api-Key',
           limit => 60, window => 60, tag => 'api';

# or a custom identity
rate_limit by => sub { my ($c) = @_; $c->session->{user} }, limit => 20;

# block an abuser from a handler; the edge drops it next time
post '/login' => sub {
    my ($c) = @_;
    if (too_many_failures($c)) { $c->block_ip(undef, 3600); }
    ...
};

DESCRIPTION

rate_limit installs a before_dispatch that answers 429 Too Many Requests (with Retry-After and the X-RateLimit-* headers) when a caller exceeds the limit for the rule. The counters live in Hyperman's shared arena, mapped before its workers fork, so a limit is exact across the whole pool rather than per worker. It is by the client IP by default; by => 'header:NAME' keys on a request header, and by => sub { ... } on whatever the coderef returns for a context. for scopes a rule to a path prefix, tag names its counter namespace. Declare it more than once for layered limits.

Blocking is separate and cheaper: $c->block_ip($ip, $ttl) adds an IP to the same arena's denylist, and Hyperman drops it at accept - before a byte is read - on its next connection. $c->unblock_ip($ip) lifts it. Both default $ip to the current request's REMOTE_ADDR. $c->rate_hit($key, $limit, $window) is the raw counter check, returning ($ok, $remaining, $reset).

Everything fails open: with no Hyperman >= ABI v3 under the application the limiter allows every request and blocking is a no-op, so it is never the reason a good request is refused.

BEHIND A REVERSE PROXY

If this application runs behind nginx, an ELB or a CDN, declare "proxy" in Punk or the limiter is wrong in a way that will take the site down.

The exactness above is what makes it dangerous. REMOTE_ADDR behind a proxy is the proxy's address on every request, and because the counter is shared across the whole worker pool rather than per worker, every client on the internet lands in one bucket: a limit => 100 rule then throttles the entire site at 100 per window, and $c->block_ip bans the load balancer.

use Punk;
proxy;                                  # one proxy in front
rate_limit limit => 100, window => 60;   # now keyed on the real client

Do not reach for by => 'header:X-Forwarded-For' instead. Nothing validates that header, so on an application that is not actually behind a proxy any client can set it and step into a fresh bucket at will - a bypass in place of a shared bucket. "proxy" in Punk validates the hop chain; this does not.

Note also that once a proxy is in front, $c->block_ip can no longer be enforced at accept: the edge sees only the proxy, so the ban becomes a 403 at dispatch. Same outcome, one request's worth of cost.

SEE ALSO

Punk, "proxy" in Punk, Hyperman.