zaber-dev/laravel-quota
Laravel Quota tracks and enforces application-level usage budgets across daily, weekly, monthly, or custom periods. Store quotas in cache or database, attach them to Eloquent models, protect routes with middleware, and extend with custom backends. Complements RateLimiter.
Architecture fit. The package is an idiomatic Laravel library: a service provider, a Quota facade backed by a Manager/Driver abstraction, immutable QuotaInfo DTOs, a HasQuotas trait with a polymorphic relation, a pre-registered quota middleware alias, typed domain events, and Prunable integration. It deliberately does not overlap with RateLimiter; RateLimiter/throttle stays for burst protection and DDoS, while this package owns cumulative budgets over calendar windows. That separation is the correct architectural framing for our stack, and it means adoption is additive rather than a refactor of existing throttling.
Integration feasibility. Very high. Composer install, one vendor:publish, optional php artisan migrate, add HasQuotas to models that meter usage, and annotate routes. Everything else is opt-in: driver selection per call (using()), events, pruning schedule. The fluent surface (Quota::for('action', $target)->limit(50)->perMonth()->consume()) and the middleware syntax (quota:exports,50,month,database) are declarative enough that non-authoring engineers can read enforcement rules from routes/api.php without a controller dive. Preconditions are only Laravel 11/12/13+ and PHP 8.2+, both cheap to verify.
Technical risk. This is a young, single-vendor, low-adoption package (110 stars, 0 dependents, first releases in 2026). The risks are not conceptual but behavioural, and several are undocumented in the README:
block() provides a time-boxed advisory lock (lockSeconds), not a guarantee. If the callback outlives the lock, a second request can acquire and both consume on success. For expensive AI/video/export jobs we must set lockSeconds well above p99 latency, not the default.array, file, and database cache stores do not behave equivalently under concurrency. The package does not publish a supported-store matrix, so read-modify-write overage is a plausible failure on a misconfigured store.cache in one code path and database in another silently maintains two independent counters. Nothing prevents this; only convention does.action:App_Models_User:12:{periodStart}:{periodEnd}). Renaming a model, introducing Relation::enforceMorphMap(), or changing the app timezone changes key derivation and effectively resets historical usage for that period.consume(). A retried queue job or a duplicated webhook double-charges a user's budget. There is no dedupe/transaction id; we must design around it.database records are, and those are pruned..ai/skills/ on boot when Laravel Boost is present. That means the package writes into the repository directory at runtime — a dirty-working-tree and CI-diff hazard on build agents.Key questions for the maintainer / spike:
INCR+EXPIRE (single round trip) or read-modify-write? Which stores are formally supported?upsert/row locks/transactions, and what is the expected behaviour under N concurrent consume() calls on one key?block()'s lock released on exception, and what error surfaces on lock-acquisition timeout — 429 or 500?periodEnd, so stale periods self-expire without cron?user() ?? ip()?Stack fit. Strong for any Laravel 11/12 service with Redis and a relational DB. The natural mapping is: Redis cache driver for high-frequency, low-stakes budgets (per-minute/hour API calls, ping-style limits); database driver for anything a customer can see, dispute, or be charged for (monthly exports, AI credits, seat caps). That dual-driver story is the package's main product advantage and should drive the adoption design.
Migration path. Do not attempt a big-bang replacement of existing counters. Phased:
cache driver, no user-facing messaging. Validates install, middleware, and 429 rendering.HasQuotas to User (and Team/Tenant if multi-tenant) and migrate one internal ad-hoc counter column to a perMonth() quota. Compare old and new counts in parallel for a full period before deleting the old column.database driver, wire QuotaConsumed/QuotaExceeded to our metrics/logging sink, and surface QuotaInfo in API responses via headers so the frontend stops hardcoding limits.RateLimiter usage that was actually doing monthly budgeting (a common mis-implementation worth auditing for).Compatibility. Keep throttle middleware in place — this package is explicitly not burst protection. Ensure Sanctum/Passport auth is resolved before CheckQuota in the middleware priority list, otherwise the middleware falls back to IP and per-user budgets become per-NAT-IP budgets. Verify timezone handling (app.timezone + NTP across nodes) before enabling perDay()/perHour() boundaries; clock skew across app servers produces different periodStart keys and inconsistent limits. If enforceMorphMap() is used anywhere, lock it in before first write, since keys are derived from resolved morph names.
Sequencing. Publish config and migrations in the same PR that pins the version. Decide the driver per action as an explicit table in an ADR — this is the artifact that prevents split-brain. Schedule model:prune for the Quota model before the DB driver's first period rolls over, not after. Then wire events, then observability, then billing-adjacent quotas. Budget roughly 1–2 days for the pilot and 1–2 sprints to reach production-grade coverage of the top handful of metered actions.
Maintenance. Small footprint: one composer dependency, one published config, one migration, one scheduled prune command. The cost is not maintenance labour but version discipline — pin to an exact version, review changelogs on bump, and treat any change to key derivation, driver semantics, or middleware consumption rules as breaking regardless of the declared SemVer. Because the licence is MIT and the surface is small, an internal fork is a realistic contingency; budget a day of spike time to confirm the driver contracts (QuotaDriverContract) are clean enough to subclass if we need DynamoDB or a stricter atomic implementation via Quota::extend().
Support. No dependents, small star count, effectively a single-vendor package: community answers will be thin and forum/Stack Overflow coverage is near-zero. Practically this means our own engineers are tier-1 support. Mitigating factors are unusually good in-repo documentation (README plus LEARN.md plus a machine-readable SKILL.md), so the knowledge is at least transferable and internalisable. The flip side: agents and engineers following SKILL.md will write consistent code, which reduces review friction but also means a documented anti-pattern propagates quickly. Keep a short internal conventions doc (which actions exist, which driver each uses, who owns the limit values) as the source of truth that overrides the package docs.
Scaling. Cache driver scales horizontally with Redis; the only concern is key cardinality per user×action×period, which is trivial. Database driver is the scaling question: one durable row per target×action×period, updated once per consume, so hot keys (a large tenant's every-request budget) create row-level write contention. Keep per-request-granularity budgets on cache and reserve the DB for coarser, human-scale budgets (monthly exports, seats). Watch the quotas table's growth and index the pruning predicate (period_end); an unscheduled prune is the most likely slow-burn incident, converting a correctness feature into an unbounded table. Multi-region deployments need a shared Redis or the cache quotas become per-region — a silent 2× limit.
Failure modes to design for.
block() → double-consume on slow jobs. Set lockSeconds from measured p99, not defaults.consume() has no idempotency key. Wrap consumption in the job's own idempotency guard..ai/skills/ → dirty build artifacts and noisy CI diffs; exclude the path in CI checks or remove Boost integration in build images.Ramp-up. Fast for a Laravel-fluent engineer: the API is self-describing and the middleware is a one-liner, so a mid-level developer is productive within a few hours and the pilot needs no dedicated training. The real ramp-up cost is conceptual, not syntactic: engineers must internalise that this package is not rate limiting, that driver choice is a correctness decision rather than a performance tweak, and that the DB driver's records are the only auditable evidence of usage. Plan a 30-minute team walkthrough covering those three points plus the failure modes above; that is a better investment than any amount of documentation reading.
How can I help you explore Laravel packages today?