Rate Limiter Designer
Workflow
1. Cartographier les surfaces à protéger
Identifier chaque endpoint par profil de risque :
| Profil |
Exemples |
Limite indicative |
| Authentification |
/login, /token, /forgot-password |
5 req/min/IP |
| Mutation sensible |
/pay, /transfer, /sms-otp |
10 req/min/user |
| Lecture lourde |
/search, /export, /report |
20 req/min/user |
| Read standard |
/products, /users/me |
300 req/min/user |
| Webhook entrant |
/webhook/* |
500 req/min/source IP |
Distinguer les identités : IP (anonyme), API key (machine-to-machine), user ID (session).
2. Choisir l'algorithme selon le besoin
| Algorithme |
Avantage |
Inconvénient |
Quand l'utiliser |
| Fixed window |
Très simple |
Burst doublé à la frontière de fenêtre |
Prototypage, admin interne |
| Sliding window log |
Précision parfaite |
RAM O(n) par client |
Haute précision, faible trafic |
| Sliding window counter |
Bon compromis |
Légère approximation (≤ fenêtre) |
API publique standard |
| Token bucket |
Burst contrôlé, filling régulier |
Implémentation plus complexe |
APIs REST, cas général |
| Leaky bucket |
Débit constant, protection aval |
Latence artificielle |
Files d'attente, systèmes fragiles en aval |
Règle de décision rapide : si le client légitime a besoin de pics courts → token bucket ; si le système aval ne tolère pas les pics → leaky bucket ; si la simplicité prime → sliding window counter.
3. Définir les tiers et quotas
# config/rate-limits.yaml
tiers:
anonymous: { rpm: 10, burst: 15 }
free: { rpm: 100, burst: 150 }
pro: { rpm: 1000, burst: 1500 }
enterprise: { rpm: 10000, burst: 15000 }
endpoints:
/auth/login: { rpm: 5, scope: ip }
/auth/forgot: { rpm: 3, scope: ip }
/api/pay: { rpm: 10, scope: user }
/api/export: { rpm: 5, scope: user, cost: 10 } # coût variable
4. Implémenter — snippets copiables
ASP.NET Core 8+ (built-in RateLimiter)
// Program.cs
builder.Services.AddRateLimiter(opt =>
{
opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
{
var key = ctx.User.Identity?.Name ?? ctx.Connection.RemoteIpAddress?.ToString() ?? "anon";
return RateLimitPartition.GetSlidingWindowLimiter(key, _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
});
});
opt.OnRejected = async (ctx, ct) =>
{
ctx.HttpContext.Response.StatusCode = 429;
ctx.HttpContext.Response.Headers.RetryAfter = "60";
await ctx.HttpContext.Response.WriteAsJsonAsync(new
{
error = "rate_limit_exceeded",
retry_after = 60
}, ct);
};
});
// Appliquer : app.UseRateLimiter();
Node.js / Express (express-rate-limit + Redis)
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
export const apiLimiter = rateLimit({
windowMs: 60_000,
max: 100,
standardHeaders: 'draft-7', // RateLimit-* headers RFC 6585
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
keyGenerator: (req) => req.user?.id ?? req.ip,
handler: (req, res) => res.status(429).json({
error: 'rate_limit_exceeded',
retry_after: Math.ceil(res.getHeader('RateLimit-Reset') as number - Date.now() / 1000)
})
});
Redis — Token Bucket en Lua (atomique)
-- token_bucket.lua (KEYS[1]=bucket_key ARGV[1]=capacity ARGV[2]=refill_rate ARGV[3]=now_ms)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2]) -- tokens/seconde
local now = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last = tonumber(bucket[2]) or now
local elapsed = (now - last) / 1000
tokens = math.min(capacity, tokens + elapsed * rate)
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate) + 10)
return 1 -- OK
else
return 0 -- rejeté
end
Appel Go/Node/C# : EVALSHA <sha> 1 user:42 100 1.67 <epoch_ms>.
5. Headers de réponse standards
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 43
RateLimit-Reset: 1719230460
RateLimit-Policy: 100;w=60
# Sur 429 :
HTTP/1.1 429 Too Many Requests
Retry-After: 17
Content-Type: application/json
Utiliser RFC 9110 Retry-After (secondes) sur tous les 429. Éviter d'exposer les compteurs précis sur les endpoints d'auth (aide les attaquants à calibrer leurs bots).
6. Rate limiting distribué (multi-instance / multi-région)
- Même région : Redis Cluster ou Redis Sentinel, scripts Lua atomiques — précision parfaite.
- Multi-région active/active : accepter une légère tolérance (±5 %) avec eventual consistency Redis Geo ou utiliser Envoy Global Rate Limiting (sidecar gRPC).
- Serverless / Edge : Cloudflare Rate Limiting (règles WAF), AWS API Gateway Usage Plans, ou Upstash Redis (HTTP-based, sans connexion persistante).
// Upstash Redis (Edge / Vercel / CF Workers)
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, "1 m"),
analytics: true,
});
const { success, remaining, reset } = await ratelimit.limit(userId);
if (!success) return new Response("Too Many Requests", { status: 429 });
7. Monitoring et observabilité
- Émettre une métrique
rate_limit.rejected_total{endpoint, tier, reason} sur chaque 429.
- Alerter si le taux de rejet dépasse 5 % sur 5 min (signal d'attaque ou de limite mal calibrée).
- Logger
{user_id, ip, endpoint, limit, window, timestamp} dans un index dédié pour post-mortem.
- Dashboard minimal : taux de rejet/min par endpoint, top-10 IP rejetées, distribution des consommations par tier.
Anti-patterns et pièges
| Piège |
Conséquence |
Correction |
Compter par IP derrière un load balancer sans X-Forwarded-For |
Tous les clients partagent la même IP (celle du LB) |
Lire X-Forwarded-For ou CF-Connecting-IP ; configurer trust proxy |
| Fixed window sans décalage aléatoire |
Thundering herd à chaque reset |
Sliding window ou jitter sur le reset |
Pas de Retry-After sur 429 |
Les clients retentent immédiatement, amplifiant la charge |
Toujours retourner Retry-After |
| Même limite pour toutes les routes |
Endpoint léger et endpoint coûteux identiques |
Coût variable (cost-per-request) ou limites par endpoint |
| In-memory sur instances multiples |
Chaque instance a son propre compteur → 10× la limite réelle |
Redis partagé obligatoire dès la deuxième instance |
| Rate limiting sans auth |
L'attaquant tourne les IPs librement |
Coupler auth + rate limiting ; CAPTCHA sur les endpoints publics sensibles |
| Limites trop strictes sans whitelist |
Blocage des crawlers légitimes, monitoring, CI/CD |
API keys internes exemptées, whitelist CIDR |
Bonnes pratiques 2026
- Cost-based limiting : chaque requête consomme N tokens selon sa complexité (LLM = coût élevé,
/ping = 0).
- Adaptive rate limiting : augmenter temporairement les limites pour les clients à comportement normal, resserrer sous attaque (ML ou règles heuristiques).
- DDoS Layer 7 : le rate limiting applicatif ne remplace pas un WAF (Cloudflare, AWS WAF) pour les attaques volumétriques.
- Tests de charge : valider les limites avec k6 ou Gatling avant la prod — les race conditions Redis se révèlent uniquement sous concurrence réelle.
- Documentation contractuelle : publier les limites dans l'OpenAPI spec (
x-rate-limit) et dans les headers dès la première requête (pas seulement sur 429).
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.
1---2name: dev-rate-limiter-designer3description: Conception de systèmes de rate limiting et throttling pour APIs — algorithmes, stockage distribué, headers standards, snippets copiables ASP.NET Core / Node / Redis. Se déclenche avec "rate limit", "throttling", "limite de requêtes", "API abuse", "DDoS protection", "quota", "429 Too Many Requests". Also triggers on "API quotas".4---56# Rate Limiter Designer78## Workflow910### 1. Cartographier les surfaces à protéger1112Identifier chaque endpoint par **profil de risque** :1314| Profil | Exemples | Limite indicative |15|--------|----------|-------------------|16| Authentification | `/login`, `/token`, `/forgot-password` | 5 req/min/IP |17| Mutation sensible | `/pay`, `/transfer`, `/sms-otp` | 10 req/min/user |18| Lecture lourde | `/search`, `/export`, `/report` | 20 req/min/user |19| Read standard | `/products`, `/users/me` | 300 req/min/user |20| Webhook entrant | `/webhook/*` | 500 req/min/source IP |2122Distinguer les identités : **IP** (anonyme), **API key** (machine-to-machine), **user ID** (session).2324---2526### 2. Choisir l'algorithme selon le besoin2728| Algorithme | Avantage | Inconvénient | Quand l'utiliser |29|------------|----------|--------------|------------------|30| Fixed window | Très simple | Burst doublé à la frontière de fenêtre | Prototypage, admin interne |31| Sliding window log | Précision parfaite | RAM O(n) par client | Haute précision, faible trafic |32| Sliding window counter | Bon compromis | Légère approximation (≤ fenêtre) | API publique standard |33| **Token bucket** | Burst contrôlé, filling régulier | Implémentation plus complexe | **APIs REST, cas général** |34| Leaky bucket | Débit constant, protection aval | Latence artificielle | Files d'attente, systèmes fragiles en aval |3536**Règle de décision rapide :** si le client légitime a besoin de pics courts → token bucket ; si le système aval ne tolère pas les pics → leaky bucket ; si la simplicité prime → sliding window counter.3738---3940### 3. Définir les tiers et quotas4142```yaml43# config/rate-limits.yaml44tiers:45 anonymous: { rpm: 10, burst: 15 }46 free: { rpm: 100, burst: 150 }47 pro: { rpm: 1000, burst: 1500 }48 enterprise: { rpm: 10000, burst: 15000 }4950endpoints:51 /auth/login: { rpm: 5, scope: ip }52 /auth/forgot: { rpm: 3, scope: ip }53 /api/pay: { rpm: 10, scope: user }54 /api/export: { rpm: 5, scope: user, cost: 10 } # coût variable55```5657---5859### 4. Implémenter — snippets copiables6061#### ASP.NET Core 8+ (built-in `RateLimiter`)6263```csharp64// Program.cs65builder.Services.AddRateLimiter(opt =>66{67 opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>68 {69 var key = ctx.User.Identity?.Name ?? ctx.Connection.RemoteIpAddress?.ToString() ?? "anon";70 return RateLimitPartition.GetSlidingWindowLimiter(key, _ => new SlidingWindowRateLimiterOptions71 {72 PermitLimit = 100,73 Window = TimeSpan.FromMinutes(1),74 SegmentsPerWindow = 6,75 QueueProcessingOrder = QueueProcessingOrder.OldestFirst,76 QueueLimit = 077 });78 });79 opt.OnRejected = async (ctx, ct) =>80 {81 ctx.HttpContext.Response.StatusCode = 429;82 ctx.HttpContext.Response.Headers.RetryAfter = "60";83 await ctx.HttpContext.Response.WriteAsJsonAsync(new84 {85 error = "rate_limit_exceeded",86 retry_after = 6087 }, ct);88 };89});90// Appliquer : app.UseRateLimiter();91```9293#### Node.js / Express (`express-rate-limit` + Redis)9495```typescript96import rateLimit from 'express-rate-limit';97import RedisStore from 'rate-limit-redis';98import { createClient } from 'redis';99100const redis = createClient({ url: process.env.REDIS_URL });101await redis.connect();102103export const apiLimiter = rateLimit({104 windowMs: 60_000,105 max: 100,106 standardHeaders: 'draft-7', // RateLimit-* headers RFC 6585107 legacyHeaders: false,108 store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),109 keyGenerator: (req) => req.user?.id ?? req.ip,110 handler: (req, res) => res.status(429).json({111 error: 'rate_limit_exceeded',112 retry_after: Math.ceil(res.getHeader('RateLimit-Reset') as number - Date.now() / 1000)113 })114});115```116117#### Redis — Token Bucket en Lua (atomique)118119```lua120-- token_bucket.lua (KEYS[1]=bucket_key ARGV[1]=capacity ARGV[2]=refill_rate ARGV[3]=now_ms)121local key = KEYS[1]122local capacity = tonumber(ARGV[1])123local rate = tonumber(ARGV[2]) -- tokens/seconde124local now = tonumber(ARGV[3])125126local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')127local tokens = tonumber(bucket[1]) or capacity128local last = tonumber(bucket[2]) or now129130local elapsed = (now - last) / 1000131tokens = math.min(capacity, tokens + elapsed * rate)132133if tokens >= 1 then134 tokens = tokens - 1135 redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)136 redis.call('EXPIRE', key, math.ceil(capacity / rate) + 10)137 return 1 -- OK138else139 return 0 -- rejeté140end141```142143Appel Go/Node/C# : `EVALSHA <sha> 1 user:42 100 1.67 <epoch_ms>`.144145---146147### 5. Headers de réponse standards148149```http150HTTP/1.1 200 OK151RateLimit-Limit: 100152RateLimit-Remaining: 43153RateLimit-Reset: 1719230460154RateLimit-Policy: 100;w=60155156# Sur 429 :157HTTP/1.1 429 Too Many Requests158Retry-After: 17159Content-Type: application/json160```161162Utiliser **RFC 9110 `Retry-After`** (secondes) sur tous les 429. Éviter d'exposer les compteurs précis sur les endpoints d'auth (aide les attaquants à calibrer leurs bots).163164---165166### 6. Rate limiting distribué (multi-instance / multi-région)167168- **Même région** : Redis Cluster ou Redis Sentinel, scripts Lua atomiques — précision parfaite.169- **Multi-région active/active** : accepter une légère tolérance (±5 %) avec eventual consistency Redis Geo ou utiliser **Envoy Global Rate Limiting** (sidecar gRPC).170- **Serverless / Edge** : Cloudflare Rate Limiting (règles WAF), AWS API Gateway Usage Plans, ou Upstash Redis (HTTP-based, sans connexion persistante).171172```typescript173// Upstash Redis (Edge / Vercel / CF Workers)174import { Ratelimit } from "@upstash/ratelimit";175import { Redis } from "@upstash/redis";176177const ratelimit = new Ratelimit({178 redis: Redis.fromEnv(),179 limiter: Ratelimit.slidingWindow(100, "1 m"),180 analytics: true,181});182183const { success, remaining, reset } = await ratelimit.limit(userId);184if (!success) return new Response("Too Many Requests", { status: 429 });185```186187---188189### 7. Monitoring et observabilité190191- Émettre une métrique `rate_limit.rejected_total{endpoint, tier, reason}` sur chaque 429.192- Alerter si le taux de rejet dépasse 5 % sur 5 min (signal d'attaque ou de limite mal calibrée).193- Logger `{user_id, ip, endpoint, limit, window, timestamp}` dans un index dédié pour post-mortem.194- Dashboard minimal : taux de rejet/min par endpoint, top-10 IP rejetées, distribution des consommations par tier.195196---197198## Anti-patterns et pièges199200| Piège | Conséquence | Correction |201|-------|-------------|------------|202| Compter par IP derrière un load balancer sans `X-Forwarded-For` | Tous les clients partagent la même IP (celle du LB) | Lire `X-Forwarded-For` ou `CF-Connecting-IP` ; configurer `trust proxy` |203| Fixed window sans décalage aléatoire | Thundering herd à chaque reset | Sliding window ou jitter sur le reset |204| Pas de `Retry-After` sur 429 | Les clients retentent immédiatement, amplifiant la charge | Toujours retourner `Retry-After` |205| Même limite pour toutes les routes | Endpoint léger et endpoint coûteux identiques | Coût variable (cost-per-request) ou limites par endpoint |206| In-memory sur instances multiples | Chaque instance a son propre compteur → 10× la limite réelle | Redis partagé obligatoire dès la deuxième instance |207| Rate limiting sans auth | L'attaquant tourne les IPs librement | Coupler auth + rate limiting ; CAPTCHA sur les endpoints publics sensibles |208| Limites trop strictes sans whitelist | Blocage des crawlers légitimes, monitoring, CI/CD | API keys internes exemptées, whitelist CIDR |209210## Bonnes pratiques 2026211212- **Cost-based limiting** : chaque requête consomme N tokens selon sa complexité (LLM = coût élevé, `/ping` = 0).213- **Adaptive rate limiting** : augmenter temporairement les limites pour les clients à comportement normal, resserrer sous attaque (ML ou règles heuristiques).214- **DDoS Layer 7** : le rate limiting applicatif ne remplace pas un WAF (Cloudflare, AWS WAF) pour les attaques volumétriques.215- **Tests de charge** : valider les limites avec k6 ou Gatling avant la prod — les race conditions Redis se révèlent uniquement sous concurrence réelle.216- **Documentation contractuelle** : publier les limites dans l'OpenAPI spec (`x-rate-limit`) et dans les headers dès la première requête (pas seulement sur 429).217218219## Communication Rules — MANDATORY220221- Ultra-concise. No filler, no preamble, no pleasantries.222- Never say "happy to help", "sure!", "great question", "let me", or similar.223- Tool first, talk second. Act before explaining.224- Result first. Lead with outcome, not process.225- Stop when done. No summary, no recap, no trailing commentary.226- No politeness wrappers. Direct and blunt.227- Minimum words. If one word works, do not use ten.228- No unsolicited explanations.229- No emoji unless asked.