Flick
Guide Validation Services API Pro Examples GitHub
Docs / Services
Flick Pro Service

Tested and maintained, so you don't have to. $99/year.

Get Flick Pro

Throttle

Put one line in front of a login, an OTP send, or a contact form and Flick counts attempts per client, blocking the ones that hammer it. limit() records the attempt and answers in one call — true means blocked, so the guard clause reads without negation.

Why Use This Service?

A public form with no rate limit is an open invitation: bots hammer a contact form, a login page, or an OTP send endpoint until something gives, whether that's your inbox, your mail quota, or an account's password.

A session-based counter doesn't stop them. A visitor who drops their cookie gets a brand new session, and with it a brand new counter — the same reason OTP's own cooldown resets when the session does. Rate limiting has to live somewhere the client can't reset on a whim.

One line in front of any form fixes it. Throttle counts attempts server-side, keyed by client identity, and blocks the ones that go over.


Installation

Flick Pro requires a license. See Pro installation for setup instructions.

Throttle needs no configuration — the default file storage works out of the box.

Configuration

All keys are optional:

$config = [
    'services' => [
        'throttle' => [
            'storage' => 'file',     // 'file' (default), 'sql', or a Flick\Throttle\Storage\CounterStorageInterface instance
            'max' => 5,              // Attempts allowed per window
            'per' => 'minutes:1',    // Window length, unit:N format
            'table' => 'throttle',   // 'sql' storage only
        ],
    ],
];

$form = new Flick($config);
Option Type Default Description
storage string, or instance 'file' Where counters are kept: 'file', 'sql', or a CounterStorageInterface instance
max int 5 Attempts allowed per window
per string 'minutes:1' Window length, unit:N format
table string 'throttle' Table name, 'sql' storage only

Supported duration formats: seconds:N, minutes:N, hours:N, days:N, weeks:N, months:N, years:N

File Storage

The default. No database needed — counters live in locked files, and nothing a visitor can reset by dropping a cookie. Two things worth knowing: it's per-server, so a load-balanced setup with more than one web server won't share counters across them, and it's cleared on reboot. Both are fine for the short, minute-long windows throttle is built for — a login form doesn't need its counter to survive a server restart. Multi-server setups need 'storage' => 'sql'.

SQL Storage

Switch storage to the SQL service so every server shares the same counters:

$config = [
    'services' => [
        'throttle' => [
            'storage' => 'sql',
        ],
        'sql' => [ /* your database config */ ],
    ],
];

Flick doesn't create the table for you — create it yourself. The column is id, not keyKEY is a reserved word in MySQL:

CREATE TABLE throttle (
    id         VARCHAR(64) NOT NULL,   -- sha256 of limiter name + client identity
    attempts   INTEGER NOT NULL DEFAULT 0,
    expires_at INTEGER NOT NULL,       -- unix timestamp, window end
    PRIMARY KEY (id)
);

To use a different table name, set table alongside storage: 'sql'.

Basic Usage

A Contact Form

One line in front of the form does everything:

if ($form->throttle->limit('contact')) {
    // Too many submits from this client.
    // Error bag ('throttle'): "Too many attempts. Try again in 45 seconds."
}

A Login Form

Forgive the counter on success, so a user who typoed their password a few times isn't locked out by their own success:

if ($form->throttle->limit('login')) {
    // blocked; render the form, the error shows
} elseif ($form->auth->verify($password, $user['password'])) {
    $form->throttle->clear('login');
    $form->auth->login($user['id']);
    $form->redirect('/dashboard');
}

Per Account

Clients are told apart by IP (proxy-aware) by default. Pass key to limit per account instead — useful against an attacker rotating IPs at one account:

$form->throttle->limit('login', ['key' => $email, 'max' => 10, 'per' => 'minutes:5']);

In Front of OTP

if ($form->ok() && ! $form->throttle->limit('otp-send')) {
    $form->otp->send($email);
}

Throttle guards the endpoint per client; OTP's own cooldown and attempt cap guard each identifier — both are already built in, so there's no need to add a second per-identifier throttle on top.

Methods

limit()

Record an attempt and answer whether this client is over the limit.

$form->throttle->limit(string $name, array $options = []): bool
Parameter Type Description
$name string The limiter's name — a form or endpoint identifier, e.g. 'login'
$options array max (default 5), per (default 'minutes:1'), key (replaces the client IP as the identity)

Returns: true if this client is over the limit — a retry message is added to the error bag under 'throttle'. false if the client can proceed.

Tip

Blocked hits keep counting but never extend the window, so the retry time Flick promises stays true.

clear()

Forgive the counter — call after a success (correct password, valid code), so a legitimate user's failed tries don't outlive their success.

$form->throttle->clear(string $name, array $options = []): void
Parameter Type Description
$name string The limiter's name
$options array key

retryIn()

Seconds until the block lifts, or null while the client is under the limit. Read-only — never counts a hit.

$form->throttle->retryIn(string $name, array $options = []): ?int
Parameter Type Description
$name string The limiter's name
$options array max, key

Returns: Seconds remaining, or null if the client hasn't reached the limit.

if ($seconds = $form->throttle->retryIn('login')) {
    echo "Locked. Try again in {$seconds} seconds.";
}

Security Features

  • Hashed Keys: Storage keys are sha256 hashes of the limiter name plus client identity — raw IPs and emails never persist in files or rows
  • Fixed Windows: Blocked hits keep counting but never extend the window, so the promised retry time stays accurate
  • Fail-Open File Storage: The default storage keeps a form usable even under filesystem pressure, rather than locking every visitor out if a counter write fails
  • Proxy-Aware Identity: Client IPs come from Flick's core request handling, which already accounts for trusted proxies — throttle doesn't second-guess it