Ce service est une API HTTP de limitation de débit (rate limiting) conçue pour protéger des routes backend contre les abus de trafic. Il permet de définir des règles par client et par route, d'identifier les appelants par session, adresse IP ou de manière globale, et d'évaluer en temps réel si une requête doit être autorisée ou rejetée. Le service expose deux algorithmes atomiques — fenêtre glissante et fenêtre fixe — implémentés via des scripts Lua exécutés directement dans Redis afin de garantir la cohérence sous forte concurrence. L'architecture hexagonale sépare strictement la logique métier des détails d'infrastructure, rendant chaque couche testable et substituable indépendamment.
Requête HTTP
│
▼
┌─────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ FastAPI │ │ Pydantic │ │ Middleware │ │
│ │ Router │───▶│ Schemas │ │ X-Request-ID │ │
│ └──────┬───────┘ └──────────────┘ └───────────────┘ │
└─────────┼───────────────────────────────────────────────────┘
│ DTO
▼
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AllowRequestUseCase / ManageClientUseCase / ... │ │
│ └──────────────────────┬──────────────────────────────┘ │
└─────────────────────────┼───────────────────────────────────┘
│ Entités du domaine
▼
┌─────────────────────────────────────────────────────────────┐
│ DOMAINE │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ RateLimitRule │ │ RateLimitEvaluator │ │
│ │ Client │ │ (service pur, sans I/O) │ │
│ │ Decision │ └────────────┬─────────────┘ │
│ └──────────────────┘ │ │
│ ┌──────────┴──────────┐ │
│ │ CounterStore (port) │ │
│ │ ClientRepo (port) │ │
│ └──────────┬───────────┘ │
└─────────────────────────────────────────┼────────────────────┘
│ Implémentation
▼
┌─────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE (Redis) │
│ │
│ ┌────────────────────────┐ ┌──────────────────────────┐ │
│ │ RedisCounterStore │ │ RedisClientRepository │ │
│ │ (Lua sliding/fixed) │ │ (Hash + JSON rules) │ │
│ └────────────┬───────────┘ └────────────┬─────────────┘ │
└───────────────┼────────────────────────────┼────────────────┘
│ │
▼ ▼
┌──────────┐
│ Redis │
└──────────┘
| Couche | Responsabilité | Dépendances |
|---|---|---|
| Domaine | Entités, value objects, logique métier pure | Aucune (stdlib uniquement) |
| Application | Orchestration des use cases, DTOs | Domaine uniquement |
| Infrastructure | HTTP (FastAPI), Redis, configuration | Application + Domaine |
La règle fondamentale : les couches internes n'importent jamais les couches externes. Le domaine ne connaît ni Redis ni FastAPI ; il définit des ports (interfaces abstraites) que l'infrastructure implémente.
Deux algorithmes sont disponibles, choisis par règle via le champ algorithm.
| Algorithme | Comportement | À utiliser quand |
|---|---|---|
sliding_window |
Fenêtre temporelle glissante — précis, pas d'effet de bord | Précision importante |
fixed_window |
Compteur réinitialisé à intervalle fixe — léger, très rapide | Performance prioritaire |
Fenêtre glissante : chaque requête est horodatée dans Redis. La fenêtre avance en continu — les entrées expirées sont supprimées avant chaque vérification.
Fenêtre fixe : un seul compteur par intervalle (ex. une clé par minute). Plus simple et moins coûteux, mais un pic est possible en chevauchement de deux intervalles.
Atomicité : les deux algorithmes s'exécutent via des scripts Lua dans Redis. La vérification et l'incrémentation sont indivisibles — aucune race condition possible.
Lorsque le CounterStore lève une exception (Redis injoignable, timeout), le RateLimitEvaluator retourne une Decision avec reason=STORAGE_UNAVAILABLE et allowed=True.
Pourquoi fail-open et non fail-closed ? Ce service est un composant transversal, non un gardien de sécurité critique. Bloquer 100 % du trafic lors d'une panne Redis causerait une indisponibilité totale de l'application hôte — un impact plus grave que laisser passer temporairement des requêtes en excès. Cette décision est délibérée et doit être documentée dans les SLO du service aval.
L'alternative classique consiste à utiliser une transaction optimiste : WATCH key → GET → calcul → MULTI/EXEC, avec retry en cas de conflit. Cette approche présente deux problèmes sous charge :
- Contention élevée : les retries se multiplient si de nombreux clients accèdent à la même clé simultanément
- Complexité client : la logique de retry alourdit le code applicatif
Les scripts Lua s'exécutent de manière sérialisée côté serveur — Redis étant single-threadé pour l'exécution des scripts — ce qui garantit l'atomicité sans aucun retry côté client, avec une latence prévisible.
Les Sorted Sets permettent trois opérations en une seule structure :
ZADD— insérer un timestamp avec score (le timestamp lui-même)ZREMRANGEBYSCORE— supprimer les entrées expirées en O(log n)ZCARD— compter les entrées actives en O(1)
Un simple LIST ou SET ne permettrait pas de filtrer efficacement par plage temporelle. Un HASH ne supporterait pas le tri naturel par timestamp.
Le service garantit la cohérence sur un nœud Redis unique. En configuration Redis Cluster (sharding), les scripts Lua multi-clés et les WATCH inter-slots ne sont pas supportés. Une migration vers Redis Cluster nécessiterait soit de router toutes les clés d'un même client vers le même slot (hash tags {client_id}), soit d'utiliser un algorithme distribué tel que Redlock pour les verrous multi-nœuds.
- Docker et Docker Compose installés
makedisponible
# Construire et démarrer l'API + Redis
docker-compose up --buildL'API est disponible sur http://localhost:8000.
La documentation interactive Swagger est accessible sur http://localhost:8000/docs.
# Tous les tests (unitaires + intégration)
make test
# Tests unitaires uniquement (sans Redis)
make test-unit
# Tests d'intégration (requiert Redis de test)
make test-integration| Variable | Défaut | Description |
|---|---|---|
REDIS_URL |
redis://localhost:6379 |
URL de connexion Redis |
APP_ENV |
development |
Environnement d'exécution |
LOG_LEVEL |
INFO |
Niveau de log |
Copier .env.example vers .env pour surcharger localement :
cp .env.example .envcurl http://localhost:8000/health
# {"status": "ok"}curl -X POST http://localhost:8000/v1/clients \
-H "Content-Type: application/json" \
-d '{"id": "mon-service", "name": "Mon Service Backend"}'Réponse 201 :
{
"id": "mon-service",
"name": "Mon Service Backend",
"created_at": "2024-01-15T10:00:00Z",
"rules": []
}curl http://localhost:8000/v1/clients/mon-servicecurl -X POST http://localhost:8000/v1/clients/mon-service/policies \
-H "Content-Type: application/json" \
-d '{
"route": "/api/videos",
"method": "GET",
"identifier_type": "session_id",
"algorithm": "sliding_window",
"limit": 100,
"window_seconds": 60
}'Paramètres :
| Champ | Type | Valeurs | Description |
|---|---|---|---|
route |
string | /api/videos, /api/* |
Route exacte ou préfixe (avec *) |
method |
string | GET, POST, * |
Méthode HTTP ou wildcard |
identifier_type |
string | none, session_id, ip_user_agent |
Stratégie d'identification |
algorithm |
string | sliding_window, fixed_window |
Algorithme de comptage |
limit |
integer | > 0 | Nombre max de requêtes autorisées |
window_seconds |
integer | > 0 | Durée de la fenêtre en secondes |
Réponse 201 :
{
"id": "3f7a1b2c-...",
"route": "/api/videos",
"method": "GET",
"identifier_type": "session_id",
"algorithm": "sliding_window",
"limit": 100,
"window_seconds": 60,
"burst": null
}curl -X DELETE http://localhost:8000/v1/clients/mon-service/policies/3f7a1b2c-...
# 204 No Contentcurl -X DELETE http://localhost:8000/v1/clients/mon-service
# 204 No Contentcurl http://localhost:8000/v1/clients/mon-service/stateRéponse 200 :
{
"client_id": "mon-service",
"rules": [
{
"id": "3f7a1b2c-...",
"route": "/api/videos",
"method": "GET",
"identifier_type": "none",
"algorithm": "sliding_window",
"limit": 100,
"window_seconds": 60,
"burst": null,
"current_count": 42
}
]
}curl -X POST http://localhost:8000/v1/evaluate \
-H "Content-Type: application/json" \
-d '{
"client_id": "mon-service",
"route": "/api/videos",
"method": "GET",
"identifier": {
"type": "session_id",
"session_id": "user-abc-123"
}
}'Réponse 200 — Requête autorisée :
{
"allowed": true,
"limit": 100,
"remaining": 57,
"reset_at": "2024-01-15T10:01:00Z",
"retry_after": null,
"rule_id": "3f7a1b2c-...",
"reason": "allowed"
}Réponse 429 — Quota dépassé :
{
"allowed": false,
"limit": 100,
"remaining": 0,
"reset_at": "2024-01-15T10:01:00Z",
"retry_after": 23,
"rule_id": "3f7a1b2c-...",
"reason": "quota_exceeded"
}Valeurs possibles pour reason :
| Valeur | Signification |
|---|---|
allowed |
Requête dans les limites |
quota_exceeded |
Limite atteinte |
no_policy |
Aucune règle ne correspond — fail-open |
storage_unavailable |
Redis injoignable — fail-open |
Types d'identifiants :
| Type | Champ(s) requis | Clé de comptage |
|---|---|---|
none |
— | Compteur global (toutes requêtes confondues) |
session_id |
session_id |
Valeur brute du session ID |
ip_user_agent |
ip, user_agent |
sha256(ip | user_agent)[:16] |
| Limitation | Impact |
|---|---|
| Nœud Redis unique | Pas de haute disponibilité native ; une panne Redis est détectée en fail-open |
| Pas de persistance configurée | Les compteurs sont perdus au redémarrage de Redis si AOF/RDB n'est pas activé |
| Inspection par identifiant global uniquement | GET /state retourne le compteur pour l'identifiant none ; les compteurs par session_id ou ip_user_agent ne sont pas agrégés |
| Pas d'authentification API | Les endpoints d'administration ne sont pas protégés |
- Support Redis Cluster — utiliser des hash tags
{client_id}pour co-localiser les clés d'un même client sur le même slot, permettant l'exécution de scripts Lua multi-clés - Persistance Redis — activer la configuration AOF (Append-Only File) pour éviter la perte de compteurs au redémarrage
- Observabilité — intégration OpenTelemetry (traces distribuées sur le chemin
evaluate) et exposition de métriques Prometheus :rate_limit_requests_total{result="allowed|rejected"},rate_limit_redis_latency_seconds - Token Bucket — implémenter un troisième algorithme permettant des bursts contrôlés (le champ
burstest déjà présent dansRateLimitRule) - Interface d'administration — dashboard pour visualiser les compteurs actifs, modifier les règles à chaud et consulter les logs de décisions
- Authentification API — HMAC ou JWT sur les routes d'administration, séparation du plan de contrôle (gestion des règles) et du plan de données (évaluation)
- Latence cible — mesurer et alerter sur la latence du chemin
POST /v1/evaluate(objectif p99 < 5 ms sur réseau local avec Redis)