Rate limiting
Every outbound call is paced through http-client-rate-limits-for-laravel, so you proactively stay under Plausible’s quotas instead of hammering the API and eating 429s. There are three independent buckets — stats (query API), events (ingestion) and sites (sites, goals and shared links) — each keyed plausible:{surface}:{owner}:{host}[:{site}]. The host comes from your instance URL, so Cloud and self-hosted never share a window.
Plausible’s limit — 600 requests per hour by default — applies per API key, across every site the key can read. So stats and sites default to one account-wide bucket (per_site off): a dashboard over 20 sites still spends one 600/hour budget, as Plausible counts it. events is unauthenticated and keeps a bucket per site.
Bucket settings
Configure each bucket under plausible.rate_limits.{surface}:
| Key | Stats | Events | Sites | Purpose |
|---|---|---|---|---|
enabled | true | true | true | false runs the plain client with no limiter. |
owner | app | app | app | Folds several app instances sharing one API key into one window. |
limit | 600 | 6000 | 60 | Max requests per window. |
per | hour | minute | hour | Window size: second, minute, hour or day. |
adaptive | true | true | true | Honour Plausible’s 429 Retry-After for server-authoritative backoff. |
max_wait | null | null | null | Milliseconds. null = pace (wait); set it to fail fast with RateLimitExceeded. |
jitter | null | null | null | Milliseconds of random spread added to defers. |
per_site | false | true | false | Include the site in the key — a separate budget per site. Keep it off for stats and sites unless your key really has per-site quotas. |
Stats defaults to Plausible’s documented 600 requests per hour per key; Events is a generous per-minute budget for high-volume ingestion; Sites is a low provisioning budget drawn from the same key. Self-hosted instances may set different numbers.
Environment variables
Each key is backed by an env variable. The stats names are shown — replace STATS with EVENTS or SITES for the other buckets. The boolean keys — enabled, adaptive and per_site — accept true/false, on/off, yes/no and 1/0; anything else throws an InvalidConfigurationException naming the key. The rest are just as strict: per must be one of second, minute, hour or day (a typo such as hourly throws instead of becoming hourly), limit is a whole number of at least 1, max_wait and jitter are whole numbers of at least 0 (or unset), and owner is a string. A blank value (PLAUSIBLE_STATS_RATELIMIT=) is not set, so its default applies:
| Key | Env variable |
|---|---|
enabled | PLAUSIBLE_STATS_RATELIMIT_ENABLED |
owner | PLAUSIBLE_RATELIMIT_OWNER (shared by all three) |
limit | PLAUSIBLE_STATS_RATELIMIT |
per | PLAUSIBLE_STATS_RATELIMIT_PER |
adaptive | PLAUSIBLE_STATS_RATELIMIT_ADAPTIVE |
max_wait | PLAUSIBLE_STATS_RATELIMIT_MAX_WAIT |
jitter | PLAUSIBLE_STATS_RATELIMIT_JITTER |
per_site | PLAUSIBLE_STATS_RATELIMIT_PER_SITE |
PLAUSIBLE_RATELIMIT_OWNER=app
PLAUSIBLE_STATS_RATELIMIT=600
PLAUSIBLE_STATS_RATELIMIT_PER=hour
PLAUSIBLE_STATS_RATELIMIT_MAX_WAIT=5000
PLAUSIBLE_EVENTS_RATELIMIT_PER=minute
PLAUSIBLE_SITES_RATELIMIT_ENABLED=false
# Booleans accept true/false, on/off, yes/no and 1/0; anything else throws.
# Only for a key with real per-site quotas:
PLAUSIBLE_STATS_RATELIMIT_PER_SITE=onWait vs fail fast
By default a request that would exceed its window waits until the window frees up. Set max_wait (ms) to instead throw RoundlyConsulting\Plausible\Exceptions\RateLimitExceeded, carrying the budget key and the wait window via retryAfterSeconds():
use RoundlyConsulting\Plausible\Exceptions\RateLimitExceeded;
try {
plausible()->stats()->aggregateResult($request);
} catch (RateLimitExceeded $e) {
report("Retry {$e->key} in {$e->retryAfterSeconds()}s");
}Because the hint rides on RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter, a host can answer any rate-limited failure — from this package or another Roundly package — with one check:
use RoundlyConsulting\PackageToolkit\Contracts\HasRetryAfter;
if ($e instanceof HasRetryAfter) {
return response('Too Many Requests', 429, ['Retry-After' => $e->retryAfterSeconds()]);
}Retries and shared budgets
- Native retry — the generic retry.* config (transient connection-error retry, off by default) is unrelated. Leave 429 backoff to the adaptive limiter so you never back off twice.
- Shared budgets — the limiter defaults to an in-memory, per-process store: fine for a single worker, CLI or scheduled report. For a budget shared across workers or servers, point its store at Cache, Redis or Database via HTTP_CLIENT_RATE_LIMITS_STORE (see http-client-rate-limits-for-laravel).
- Per-site quotas — per_site is off for stats and sites because Plausible counts requests per API key; turn it on only if your key really has per-site quotas.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.