Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Api Platform Rate Limiter Laravel Package

jacyimp/api-platform-rate-limiter

A Laravel/PHP rate limiter for API platforms, providing middleware-driven request throttling to protect endpoints and control traffic. Lightweight, easy to integrate, and configurable to fit existing API stacks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and confirm which version you're on — v0.2.0 is a breaking release, so composer require on a fresh project gets you the new API while any pinned ^0.1 dependency will not behave the same.

composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config   # if the package ships config

Where to look first, in this order:

  1. The RateLimit value object constructor. It is now the entire public surface: limit, interval, cost, bucketResolver, plus the dynamic resolver options. Everything removed in v0.2.0 (DynamicLimit, DynamicCost, DynamicBucket, Identity, Condition, Interval) collapsed into named parameters here.
  2. The resolver contracts. Dynamic values are now expressed as class-string<T> — so the package's resolver interfaces are what you implement, not what you extend or configure.
  3. The middleware / limiter registration in the service provider, so you know the order limits are evaluated and where the 429 is emitted.

First use case — put a single limit on one resource and confirm you can trip it:

new Post(
    extraProperties: [
        new RateLimit(limit: 5, interval: '1 minute'),
    ],
);

Hit the endpoint six times, expect a 429 with rate-limit headers, then delete the limit. Do this before wiring anything else; it validates that the package can resolve identity in your app (see the Symfony Security gotcha below — this is the step where that bites).

Implementation Patterns

Stack multiple windows instead of picking one. The v0.2.0 API is designed for composition, and this is now the idiomatic shape:

new Post(
    extraProperties: [
        new RateLimit(limit: 100,    interval: '1 minute'),
        new RateLimit(limit: 1_000,  interval: '1 hour'),
        new RateLimit(limit: 10_000, interval: '1 day'),
    ],
);

All declared limits must pass. There is no "first match wins" — think of them as independent counters, not tiers.

Resource-level default + operation-level tightening. Set a coarse ceiling once on the resource class, then add narrower limits on expensive operations (search, export, bulk write). Because resource-level and operation-level limits now combine rather than replace, the resource limit acts as a floor of protection you can't accidentally drop by adding a per-operation limit — but it also means an operation limit can never raise the ceiling.

Separate bucket from identity deliberately. The v0.2.0 semantics are explicit: identity is who is being limited, bucket is which counter they hit. For tenant-aware limits, use bucketResolver to derive the bucket (e.g. tenant:{id}) while identity stays the user or IP. Getting this backwards is how you end up with one tenant exhausting another tenant's quota or with per-tenant limits that are actually global.

new RateLimit(
    limit: 1_000,
    interval: '1 hour',
    bucketResolver: TenantBucketResolver::class, // class-string<T>
);

Dynamic values as resolver class names. Anything that varies per request (cost of an operation, limit from a plan table) is a resolver class string, resolved from the container. Keep them stateless and cheap — a constructor that queries the database will run on every limited request.

Laravel integration tips:

  • Register the package middleware on the route groups that need limiting rather than globally, so health checks and webhooks aren't burning quota.
  • Wrap expensive endpoints in throttle-style middleware groups and keep the package's limits as the business limits (plan quotas) — don't duplicate Laravel's built-in throttle for the same purpose.
  • For per-user limits in a Laravel app, be explicit: don't rely on the default identity resolution (see gotchas).
  • Test by driving a limit to a low number (limit: 2) and asserting the 429 and headers, rather than hammering a realistic limit — and flush the cache store between tests, since counters survive RefreshDatabase.

Migration pattern from v0.1.x: delete every Dynamic*, Identity, Condition, and Interval usage; fold each DynamicLimit into a RateLimit whose limit:/cost: is a resolver class-string; replace DynamicBucket with bucketResolver:; replace new Interval(...) with a string like '1 hour' or a native DateInterval; remove RateLimitChecking listeners and move their side effects into middleware or a 429 response handler.

Gotchas and Tips

Limits combine — audit your numbers after upgrading. If v0.1.x relied on an operation-level limit replacing a resource-level one, the effective quota in v0.2.0 is now the intersection. Expect newly over-restrictive endpoints immediately after the upgrade and check them before shipping.

Symfony Security is optional, and on Laravel it isn't installed. The release notes state authenticated Symfony users are limited by user identity when Security is present, and applications without it fall back cleanly to client IP. In a Laravel app that fallback is your default. If you want per-user limits, you must supply identity yourself via a resolver — otherwise every user behind a NAT behaves as one client.

The IP fallback makes TrustProxies load-bearing. Behind a load balancer, CDN, or API gateway, the resolved IP can be the proxy's, collapsing all traffic into a single bucket and causing "phantom" 429s under modest load. Check App\Http\Middleware\TrustProxies and the $proxies/$headers configuration before blaming the package.

RateLimitChecking is gone. Anything you hung off that event (metrics, logging, alerting on near-limit traffic, custom rejection payloads) needs a new home: a middleware after the limiter, a listener on Laravel's exception handler for ThrottleRequestsException/429 responses, or the package's response headers.

Interval strings vs DateInterval. Strings are the readable default ('1 minute', '1 hour', '1 day'). If you pass a native DateInterval, avoid month/year units — they aren't fixed durations and make the window boundary non-deterministic between requests. Stick to seconds/minutes/hours/days.

A resolver that returns an empty or null bucket silently shares a counter. Guard your bucketResolver implementations: a missing tenant ID shouldn't fall through to a global bucket. Fail loudly (throw) or return a distinct fallback key.

Resolvers are container-resolved per request. They can't hold request state across calls, and they can't be closures — they're class-string<T>, so the class must be autoloadable and constructible. This also means an unresolvable constructor dependency turns into a runtime error on a rate-limited route, not at boot; add a smoke test that hits each limited endpoint once.

Costs multiply against every declared window. A dynamic cost resolver returning 10 on a route with three windows decrements all three by 10. Verify that's what you want for batch endpoints before shipping a bulk import behind a limit.

Debugging checklist for an unexpected 429: confirm which identity resolved (log user ID vs IP), confirm the bucket key, confirm the proxy/TrustProxies setup, then confirm whether another limit on the same resource is the one tripping. Headers on the 429 (remaining/limit/reset, Retry-After) tell you which window fired faster than reading code.

Extension points that survived the simplification: resolver class strings and bucketResolver are now the only intended customisation surface. If you find yourself wanting a hook that no longer exists, the package's answer is composition — declare another RateLimit — not lifecycle events.

Pin the major. Given a 0.x line that just removed five public classes, constrain to ^0.2 and read the changelog before each minor.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect