jacyimp / api-platform-rate-limiter
Flexible rate limiting for API Platform
Package info
github.com/jacyimp/api-platform-rate-limiter
Type:symfony-bundle
pkg:composer/jacyimp/api-platform-rate-limiter
Requires
- php: ^8.2
- api-platform/metadata: ^3.4 || ^4.0
- psr/event-dispatcher: ^1.0
- symfony/config: ^6.4 || ^7.0 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.0 || ^8.0
- symfony/http-foundation: ^6.4 || ^7.0 || ^8.0
- symfony/http-kernel: ^6.4 || ^7.0 || ^8.0
- symfony/rate-limiter: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- bamarni/composer-bin-plugin: ^1.9
- behat/behat: ^3.32
- illuminate/cache: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/routing: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- paragonie/random_compat: ^9.99
- phpstan/phpstan: ^2.2.12
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^11.5.50
- slevomat/coding-standard: ^8.31
- squizlabs/php_codesniffer: ^4.0.4
- symfony/framework-bundle: ^6.4 || ^7.0 || ^8.0
- symfony/security-core: ^6.4 || ^7.0 || ^8.0
Suggests
- api-platform/laravel: Required when using the Laravel integration (^4.2 or ^4.3).
- symfony/security-core: Required to rate limit authenticated Symfony users by user identity.
Provides
None
Conflicts
None
Replaces
None
README
Rate limiting for API Platform applications on Symfony and Laravel.
Define quotas next to API Platform operations, share a quota across endpoints, or apply limits to the whole API. With Symfony Security or Laravel authentication available, requests are limited by authenticated user and then by client IP by default.
This package is pre-1.0. Its public API may still change between releases.
Requirements
- PHP 8.2+
- API Platform metadata 3.4 or 4.x
- Symfony 6.4, 7.x or 8.x
- or Laravel 11.x, 12.x or 13.x with API Platform for Laravel
Symfony installation
composer require jacyimp/api-platform-rate-limiter
If Symfony Flex is not available, register the bundle manually in config/bundles.php:
<?php // config/bundles.php use JacyImp\ApiPlatformRateLimiter\Symfony\ApiPlatformRateLimiterBundle; return [ // ... ApiPlatformRateLimiterBundle::class => ['all' => true], ];
Operation-local limits work immediately. No package configuration is required.
Laravel installation
composer require api-platform/laravel jacyimp/api-platform-rate-limiter
Laravel package discovery registers the service provider and API Platform middleware automatically.
Publish the configuration when you need global limits, configured buckets, custom storage, providers, bypasses, or runtime resolvers:
php artisan vendor:publish --tag=api-platform-rate-limiter-config
Add your first limit
Add RateLimit to an operation's extraProperties:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new GetCollection(), new Get( extraProperties: [ new RateLimit( limit: 100, interval: '1 minute', ), ], ), ], )] final class Product { // ... }
The item GET now allows 100 requests per minute for each resolved identity. The collection operation is unaffected.
Who is counted
With Symfony Security installed, the Symfony default identity is:
- the authenticated user identifier;
- otherwise the client IP.
Without Symfony Security, Symfony transparently uses the client IP. Installing Security is only needed for automatic authenticated-user identity resolution. Laravel uses the authenticated user identifier with the same client-IP fallback.
Symfony uses its trusted-proxy-aware Request::getClientIp() result and Laravel uses Request::ip(). Configure trusted proxies in the host framework and never parse forwarded IP headers yourself. See Choosing who gets rate limited for explicit IP, user, API key, tenant, fallback, and composite identities.
Limit every operation on a resource
Put the metadata on the resource to apply it to all of its operations:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new GetCollection(), new Get(), new Post(), ], extraProperties: [ new RateLimit( limit: 100, interval: '1 minute', ), ], )] final class Product { // ... }
Resource limits are baseline limits. Operation metadata is appended to them, so an operation enforces both the resource limits and its own limits:
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
new Post(
extraProperties: [
new RateLimit(
limit: 100,
interval: '1 minute',
),
],
),
],
extraProperties: [
new RateLimit(
limit: 10000,
interval: '1 day',
),
],
)]
final class Product
{
// ...
}
GET uses the 10000/day resource limit. POST uses both 10000/day and
100/minute, in that order.
Limit the whole API
Configured globals apply to every API Platform operation.
Symfony:
# config/packages/api_platform_rate_limiter.yaml api_platform_rate_limiter: globals: burst: limit: 100 interval: '1 minute' daily: limit: 10000 interval: '1 day'
Laravel:
<?php // config/api-platform-rate-limiter.php return [ // ... 'globals' => [ 'burst' => [ 'limit' => 100, 'interval' => '1 minute', ], 'daily' => [ 'limit' => 10_000, 'interval' => '1 day', ], ], ];
Globals are independent quotas. A resource or operation can still add a tighter local limit.
Share one quota across operations
Configure a named bucket once when several endpoints should consume the same counter.
Symfony:
# config/packages/api_platform_rate_limiter.yaml api_platform_rate_limiter: buckets: catalog: limit: 1000 interval: '1 minute'
Laravel:
<?php // config/api-platform-rate-limiter.php return [ // ... 'buckets' => [ 'catalog' => [ 'limit' => 1000, 'interval' => '1 minute', ], ], ];
Reference that bucket from each operation:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new GetCollection( extraProperties: [ new RateLimit(bucket: 'catalog'), ], ), new Get( extraProperties: [ new RateLimit(bucket: 'catalog'), ], ), ], )] final class Product { // ... }
Both operations consume the same 1,000-request quota.
For a small number of operations, the shared definition can stay inline. Repeat the same bucket, limit, and interval wherever the quota is used:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new GetCollection( extraProperties: [ new RateLimit( bucket: 'catalog', limit: 1000, interval: '1 minute', ), ], ), new Get( extraProperties: [ new RateLimit( bucket: 'catalog', limit: 1000, interval: '1 minute', ), ], ), ], )] final class Product { // ... }
Prefer a configured bucket when the definition is used widely or should be changed centrally.
Combine several limits
Add multiple metadata entries to enforce several quotas on one operation:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new Get( extraProperties: [ new RateLimit( limit: 20, interval: '1 minute', ), new RateLimit(bucket: 'catalog'), ], ), ], )] final class Product { // ... }
Limits are consumed in order. If a later limit rejects the request, consumption from earlier limits is not rolled back.
Charge expensive requests more
cost is the number of tokens consumed by one request. This lets an export and an ordinary read share a quota without costing the same amount:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use JacyImp\ApiPlatformRateLimiter\Metadata\RateLimit; #[ApiResource( operations: [ new GetCollection( name: 'product_list', extraProperties: [ new RateLimit( bucket: 'catalog', cost: 1, ), ], ), new GetCollection( uriTemplate: '/products/export', name: 'product_export', extraProperties: [ new RateLimit( bucket: 'catalog', cost: 10, ), ], ), ], )] final class Product { // ... }
Here an export consumes 10 tokens from the configured catalog bucket.
Rejection response
The default response is 429 Too Many Requests with these headers:
Retry-After
RateLimit-Limit
RateLimit-Remaining
Retry-After is an HTTP date. The body is rendered by the host framework; Laravel's default JSON body is {"message":"Rate limit exceeded."}. See Storage and production deployment to replace the rejection handler.
Guides
- Swagger / OpenAPI — automatic rate-limit descriptions.
- Quotas and shared limits — endpoint, resource, global, and shared quotas.
- Choosing who gets rate limited — users, IPs, API keys, tenants, fallback, and composite identities.
- Plans, tenants, and dynamic quotas — subscription tiers, tenant identities, dynamic bucket selection, and request costs.
- Conditional limits and bypasses — conditional rules, exempt endpoints, internal traffic, and trusted crawlers.
- Storage and production deployment — storage, Redis/shared counters, framework configuration, and rejection responses.
- Extending the rate limiter — providers, events, custom handlers, framework registration, and counter internals.
Development
composer require jacyimp/api-platform-rate-limiter:dev-main
composer check composer audit
Individual checks:
composer cs
composer analyse
composer test
composer test:behaviour
composer mutation
License
MIT