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 Rate Limit Bundle Laravel Package

indragunawan/api-rate-limit-bundle

A PHP package for adding configurable rate limiting to APIs. Define request limits, track usage per endpoint, and protect your application from abuse with simple throttling and clear rate-limit responses. Suitable for REST APIs that need lightweight, customizable request control.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites. PHP 7.4+ (8.x preferred), Symfony 5.2+ with a PSR-6 cache pool or Redis available, and API Platform 2.6/2.7. The bundle's last release was April 2021, so confirm it still resolves against your API Platform version before you build on it — see Gotchas.

Install and register.

composer require indragunawan/api-rate-limit-bundle

Flex (or a manual edit to config/bundles.php) registers the bundle:

IndraGunawan\ApiRateLimitBundle\ApiRateLimitBundle::class => ['all' => true],

Minimum viable config — config/packages/api_rate_limit.yaml:

api_rate_limit:
    enabled: true
    default_limit: 60
    default_period: 60   # seconds
    cache: cache.app     # point this at a Redis-backed pool in production

First use case. Rate-limit the single endpoint that costs you CPU or money — usually a collection/search operation. Annotate the resource (attribute shown; older API Platform releases use the analogous annotation):

use IndraGunawan\ApiRateLimitBundle\Attribute\RateLimit;

#[ApiResource]
#[RateLimit(limit: 30, period: 60)]
class SearchResult
{
    // ...
}

Then exercise it with curl -i in a loop and watch for the X-RateLimit-* headers and the 429 on the request after the limit. If you don't see them, the limiter isn't wired up — stop and debug before adding more limits.

Where to look first, in order:

  1. The README for the canonical config shape.
  2. DependencyInjection/Configuration.php — the only authoritative list of config keys and defaults.
  3. The attribute/annotation class — what knobs exist per resource (limit, period, maybe key).
  4. The event subscriber/listener that enforces the limit — search src/ for 429 or TooManyRequests. This is the class you decorate or override when defaults don't fit.

Implementation Patterns

Resource default, operation override. Set a sane default_limit at the bundle level, a tighter limit on the resource, and tighter still on write operations:

#[ApiResource(operations: [
    new GetCollection(),
    new Post(),
])]
#[RateLimit(limit: 300, period: 60)]
class Order
{
    // Post operation can get its own attribute in most versions
}

Tiered windows for burst vs. sustained. Pair a short window with a long one (e.g. 10/min and 500/hour) rather than one large window. Clients that hammer get cut off quickly; legitimate batch workers still have headroom.

Pick the key deliberately. The default is usually client IP, which is wrong for authenticated APIs (all users behind the same office NAT share a bucket) and too generous for anonymous endpoints. Configure or extend key resolution:

  • Anonymous → IP (behind proxies, see Gotchas).
  • Authenticated → user identifier from the security token.
  • Machine clients → API key or client ID.
  • Shared accounts → composite key (user_id + route) so one hot endpoint doesn't starve the rest.

Storage choice.

  • Single node / low traffic: cache.app is fine.
  • Multi-node or behind a load balancer: Redis (or another shared adapter). Anything request-local (ArrayAdapter) is useless under PHP-FPM because the counter resets every request.
  • Namespace the pool per environment so staging traffic never pollutes production counters.

Give clients a contract. Ensure the response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (or equivalent) on successful responses too, plus Retry-After on the 429. Document those headers and the 429 response in your OpenAPI decorators so SDK consumers can back off correctly.

Keep non-API routes consistent. The bundle only covers API Platform resources. If you also expose plain controllers, mirror the policy with Symfony's RateLimiter component or a kernel.request listener so limits are uniform across the app.

Testing pattern. A functional test is the cheapest regression guard:

public function testRateLimitHeadersAnd429(): void
{
    for ($i = 0; $i < 30; $i++) {
        $this->client->request('GET', '/api/search_results');
        $this->assertResponseIsSuccessful();
    }
    $this->client->request('GET', '/api/search_results');
    $this->assertResponseStatusCodeSame(429);
    $this->assertResponseHeaderSame('Retry-After', '60');
}

Reset the limiter storage in setUp/tearDown (clear the pool or flush the test Redis DB), and use unique client keys per test.

Observability. Log every 429 with the resolved key and route, and emit a counter metric. A silent limit is a mystery outage waiting to happen.

Gotchas and Tips

  • The package is unmaintained (last release 2021). API Platform 3.x changed resource metadata to the operations-based system; the bundle's subscriber may not see your #[RateLimit] on operations, or may fail to boot at all. Pin API Platform 2.7 if you must use it, or fork and adapt the metadata-reading code. Budget an afternoon for this before adopting it widely.
  • Only API Platform routes are covered. Custom controllers, messenger workers, and console commands bypass it entirely. Don't assume the bundle protects your whole surface.
  • Proxy IPs collapse your buckets. Behind a load balancer or CDN, every request looks like the same IP unless you configure Request::setTrustedProxies() (or framework.trusted_proxies) correctly. Verify by inspecting the resolved key in logs.
  • Fixed vs. sliding window. Check the implementation before promising clients a reset time. A fixed window lets a client burst 2× the limit around the boundary.
  • Cache pool mistakes are silent. A misconfigured pool name typically degrades to a fresh array adapter — limits appear to "not work" rather than erroring. Confirm the pool service ID resolves and the backing store is shared.
  • Don't share counters across environments. Include the environment (or a prefix) in the key or use separate Redis databases; otherwise staging tests burn production limits.
  • Internal sub-requests and retries double-count. ESI, forwarded sub-requests, or client retries on 5xx all consume quota. Exclude internal requests via the listener or by checking $request->attributes->get('_route').
  • 429 responses can get swallowed by the error normalizer. If API Platform's exception normalizer handles the response, you can lose the rate-limit headers. Raise the listener priority or add the headers in a response listener.
  • Config cache. After editing api_rate_limit.yaml, run bin/console cache:clear — Symfony serves the compiled config otherwise and you'll chase ghosts.
  • Extension points. Before forking, check whether you can (a) decorate the key resolver, (b) decorate the limiter service, (c) subclass/replace the event subscriber via a compiler pass, or (d) add a higher-priority kernel.response listener for custom headers. These usually cover 90% of real-world needs without touching upstream code.
  • Watch for double-limiting. If you also enforce limits in nginx/WAF, debug 429s by checking which layer produced them — the response body and headers usually differ, but only if you made them differ.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource