NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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}:

KeyStatsEventsSitesPurpose
enabledtruetruetruefalse runs the plain client with no limiter.
ownerappappappFolds several app instances sharing one API key into one window.
limit600600060Max requests per window.
perhourminutehourWindow size: second, minute, hour or day.
adaptivetruetruetrueHonour Plausible’s 429 Retry-After for server-authoritative backoff.
max_waitnullnullnullMilliseconds. null = pace (wait); set it to fail fast with RateLimitExceeded.
jitternullnullnullMilliseconds of random spread added to defers.
per_sitefalsetruefalseInclude 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:

KeyEnv variable
enabledPLAUSIBLE_STATS_RATELIMIT_ENABLED
ownerPLAUSIBLE_RATELIMIT_OWNER (shared by all three)
limitPLAUSIBLE_STATS_RATELIMIT
perPLAUSIBLE_STATS_RATELIMIT_PER
adaptivePLAUSIBLE_STATS_RATELIMIT_ADAPTIVE
max_waitPLAUSIBLE_STATS_RATELIMIT_MAX_WAIT
jitterPLAUSIBLE_STATS_RATELIMIT_JITTER
per_sitePLAUSIBLE_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=on

Wait 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 crypto

By 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.