br-rate-limiting
Configure better-route 1.1 RateLimitMiddleware with atomic fixed-window storage. Use for WpObjectCacheRateLimiter, TransientRateLimiter, persistent external object cache checks, wp_cache_incr, MySQL named locks, identity/native WordPress/IP keys, trusted proxies, Retry-After and
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/better-route/br-rate-limiting
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
better-route: rate limiting
Use a fixed-window limiter with an atomic backend. Pick the backend from deployment capabilities; do not silently fall back from an atomic store to a racy read/modify/write.
Persistent object cache
use BetterRoute\Middleware\RateLimit\RateLimitMiddleware;
use BetterRoute\Middleware\RateLimit\WpObjectCacheRateLimiter;
$rateLimit = new RateLimitMiddleware(
limiter: new WpObjectCacheRateLimiter(group: 'myapp_rate_limit'),
limit: 60,
windowSeconds: 60,
);
$router->get('/account', $handler)
->protectedByMiddleware('bearerAuth')
->middleware([$auth, $rateLimit]);
WpObjectCacheRateLimiter requires:
- WordPress cache functions;
wp_using_ext_object_cache() === truewhen that function exists;wp_cache_incr();- a backend whose increment actually behaves atomically.
Construction or a failed increment throws. Use it with a verified Redis/Memcached-style persistent backend, not WordPress's request-local default object cache.
Transient backend
use BetterRoute\Middleware\RateLimit\TransientRateLimiter;
$rateLimit = new RateLimitMiddleware(
limiter: new TransientRateLimiter(),
limit: 20,
windowSeconds: 60,
);
In default WordPress mode, TransientRateLimiter wraps the transient read/modify/write in a MySQL GET_LOCK/RELEASE_LOCK critical section. It requires global $wpdb and may throw when the lock cannot be acquired or state cannot be persisted.
If custom getTransient/setTransient callbacks are injected, also inject a real synchronize callback when requests can run concurrently. Without it, the custom mode executes unsynchronized.
Default key in 1.1
The default key deeply canonicalizes the route and the first available identity:
- auth middleware user ID;
- auth subject;
- explicit context/native logged-in WordPress user ID;
- HMAC key identity;
- resolved client IP;
guestonly when no identity or IP is available.
This means cookie/application-password/native WordPress users get per-user buckets even without an attributes['auth'] entry. Anonymous callers normally get per-IP rather than one global guest bucket.
Run auth before rate limiting when token identity should win over IP:
->middleware([$auth, $rateLimit])
Trusted client IP
Use TrustedProxyClientIpResolver behind proxies:
use BetterRoute\Middleware\Network\TrustedProxyClientIpResolver;
$ipResolver = new TrustedProxyClientIpResolver(
trustedProxyCidrs: ['10.0.0.0/24', '2001:db8:1234::/48'],
forwardedHeaders: ['CF-Connecting-IP', 'X-Forwarded-For'],
);
$rateLimit = new RateLimitMiddleware(
limiter: $limiter,
limit: 60,
windowSeconds: 60,
clientIpResolver: $ipResolver,
);
The resolver reads a forwarded header only when immediate REMOTE_ADDR is trusted. For hop lists it walks right-to-left and returns the closest untrusted address, avoiding a client-forged leftmost value. Keep provider CIDRs current.
Response contract
Allowed responses receive:
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
Denied requests return 429 rate_limited, the same rate-limit headers, and Retry-After calculated from reset time. Browser clients must include Retry-After in CORS exposedHeaders if JavaScript needs it.
The middleware preserves headers on Better Route responses, WP_REST_Response, and raw array/scalar results.
Custom keys
$rateLimit = new RateLimitMiddleware(
limiter: $limiter,
limit: 100,
windowSeconds: 60,
keyResolver: static fn ($context): string => hash('sha256', json_encode([
'route' => $context->routePath,
'tenant' => current_tenant_id(),
'user' => get_current_user_id(),
], JSON_THROW_ON_ERROR)),
);
Include route/tenant/identity deliberately and use unambiguous structured encoding. A constant global key lets one caller exhaust the bucket for everyone.
Review checklist
- Verify backend atomicity under concurrency.
- Run auth before the limiter for per-token/user limits.
- Configure trusted proxy CIDRs before trusting forwarded headers.
- Test first, last allowed, and first denied request; assert remaining/reset/retry headers.
- Test an anonymous caller from two IPs and two authenticated users.
- Monitor MySQL named-lock or cache increment failures; they are availability failures, not permission denials.
- Use upstream/CDN protection as well; PHP-level rate limiting is not volumetric DDoS mitigation.
Related skills
- Use
br-network-securityfor proxy/CIDR rules. - Use
br-auth-middlewarefor identity ordering. - Use
br-cors-public-clientto expose rate-limit headers.
References
- Verified source paths:
src/Middleware/RateLimit/RateLimitMiddleware.phpsrc/Middleware/RateLimit/WpObjectCacheRateLimiter.phpsrc/Middleware/RateLimit/TransientRateLimiter.phpsrc/Support/RequestIdentity.phpsrc/Middleware/Network/TrustedProxyClientIpResolver.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 218 B
interface: display_name: "Better Route Rate Limiting" short_description: "Throttle better-route endpoints safely." default_prompt: "Use better-route rate limiting with identity-aware keys and trusted proxy IPs."
-
-
SKILL.md 5.6 KB
--- name: br-rate-limiting description: Configure better-route 1.1 RateLimitMiddleware with atomic fixed-window storage. Use for WpObjectCacheRateLimiter, TransientRateLimiter, persistent external object cache checks, wp_cache_incr, MySQL named locks, identity/native WordPress/IP keys, trusted proxies, Retry-After and X-RateLimit headers, custom key resolvers, or diagnosing shared guest buckets and race-prone rate limiting. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "better-route" wp-skills-plugin-version-tested: "1.1.0" wp-skills-php-min: "8.1" wp-skills-last-updated: "2026-07-13" --- # better-route: rate limiting Use a fixed-window limiter with an atomic backend. Pick the backend from deployment capabilities; do not silently fall back from an atomic store to a racy read/modify/write. ## Persistent object cache ```php use BetterRoute\Middleware\RateLimit\RateLimitMiddleware; use BetterRoute\Middleware\RateLimit\WpObjectCacheRateLimiter; $rateLimit = new RateLimitMiddleware( limiter: new WpObjectCacheRateLimiter(group: 'myapp_rate_limit'), limit: 60, windowSeconds: 60, ); $router->get('/account', $handler) ->protectedByMiddleware('bearerAuth') ->middleware([$auth, $rateLimit]); ``` `WpObjectCacheRateLimiter` requires: - WordPress cache functions; - `wp_using_ext_object_cache() === true` when that function exists; - `wp_cache_incr()`; - a backend whose increment actually behaves atomically. Construction or a failed increment throws. Use it with a verified Redis/Memcached-style persistent backend, not WordPress's request-local default object cache. ## Transient backend ```php use BetterRoute\Middleware\RateLimit\TransientRateLimiter; $rateLimit = new RateLimitMiddleware( limiter: new TransientRateLimiter(), limit: 20, windowSeconds: 60, ); ``` In default WordPress mode, `TransientRateLimiter` wraps the transient read/modify/write in a MySQL `GET_LOCK`/`RELEASE_LOCK` critical section. It requires global `$wpdb` and may throw when the lock cannot be acquired or state cannot be persisted. If custom `getTransient`/`setTransient` callbacks are injected, also inject a real `synchronize` callback when requests can run concurrently. Without it, the custom mode executes unsynchronized. ## Default key in 1.1 The default key deeply canonicalizes the route and the first available identity: 1. auth middleware user ID; 2. auth subject; 3. explicit context/native logged-in WordPress user ID; 4. HMAC key identity; 5. resolved client IP; 6. `guest` only when no identity or IP is available. This means cookie/application-password/native WordPress users get per-user buckets even without an `attributes['auth']` entry. Anonymous callers normally get per-IP rather than one global guest bucket. Run auth before rate limiting when token identity should win over IP: ```php ->middleware([$auth, $rateLimit]) ``` ## Trusted client IP Use `TrustedProxyClientIpResolver` behind proxies: ```php use BetterRoute\Middleware\Network\TrustedProxyClientIpResolver; $ipResolver = new TrustedProxyClientIpResolver( trustedProxyCidrs: ['10.0.0.0/24', '2001:db8:1234::/48'], forwardedHeaders: ['CF-Connecting-IP', 'X-Forwarded-For'], ); $rateLimit = new RateLimitMiddleware( limiter: $limiter, limit: 60, windowSeconds: 60, clientIpResolver: $ipResolver, ); ``` The resolver reads a forwarded header only when immediate `REMOTE_ADDR` is trusted. For hop lists it walks right-to-left and returns the closest untrusted address, avoiding a client-forged leftmost value. Keep provider CIDRs current. ## Response contract Allowed responses receive: ```text X-RateLimit-Limit X-RateLimit-Remaining X-RateLimit-Reset ``` Denied requests return `429 rate_limited`, the same rate-limit headers, and `Retry-After` calculated from reset time. Browser clients must include `Retry-After` in CORS `exposedHeaders` if JavaScript needs it. The middleware preserves headers on Better Route responses, `WP_REST_Response`, and raw array/scalar results. ## Custom keys ```php $rateLimit = new RateLimitMiddleware( limiter: $limiter, limit: 100, windowSeconds: 60, keyResolver: static fn ($context): string => hash('sha256', json_encode([ 'route' => $context->routePath, 'tenant' => current_tenant_id(), 'user' => get_current_user_id(), ], JSON_THROW_ON_ERROR)), ); ``` Include route/tenant/identity deliberately and use unambiguous structured encoding. A constant global key lets one caller exhaust the bucket for everyone. ## Review checklist - Verify backend atomicity under concurrency. - Run auth before the limiter for per-token/user limits. - Configure trusted proxy CIDRs before trusting forwarded headers. - Test first, last allowed, and first denied request; assert remaining/reset/retry headers. - Test an anonymous caller from two IPs and two authenticated users. - Monitor MySQL named-lock or cache increment failures; they are availability failures, not permission denials. - Use upstream/CDN protection as well; PHP-level rate limiting is not volumetric DDoS mitigation. ## Related skills - Use `br-network-security` for proxy/CIDR rules. - Use `br-auth-middleware` for identity ordering. - Use `br-cors-public-client` to expose rate-limit headers. ## References - Verified source paths: - `src/Middleware/RateLimit/RateLimitMiddleware.php` - `src/Middleware/RateLimit/WpObjectCacheRateLimiter.php` - `src/Middleware/RateLimit/TransientRateLimiter.php` - `src/Support/RequestIdentity.php` - `src/Middleware/Network/TrustedProxyClientIpResolver.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.