# Backend — Risques & vigilance : Redis

> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.

---

<a id="risque-redis-thrash-connexion"></a>
## Redis — thrash de connexion sous charge

### Risques

- Connexions concurrentes multiples si `connect()` est appelé "à la demande" sans lock
- Spam logs + saturation connexions quand Redis est down ou lent

### Symptômes

- N appels simultanés → N tentatives de connexion en parallèle
- Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis

### Bonnes pratiques / mitigations

```typescript
// Pattern single-flight + cooldown + fallback DB best-effort
if (!this.connectPromise) {
  this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; });
}
await this.connectPromise;
// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB
```

- Contexte technique : Redis / NestJS — 09-03-2026

---

<a id="risque-entitlements-ttl-sla"></a>
## Entitlements — TTL cache supérieur au SLA de propagation

### Risques

- TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti)
- Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel

### Symptômes

- Accès premium encore actif après annulation (ou inversement)
- NFR "propagation ≤ 60s" non respecté en cas de webhook manqué

### Bonnes pratiques / mitigations

- TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max)
- Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre)
- Contexte technique : Redis / entitlements / NestJS — 09-03-2026

---

<a id="risque-compteurs-inmemory"></a>
## Compteurs in-memory ≠ métriques persistées

### Risques

- Compteurs in-memory remis à zéro au restart (perte de données)
- Non agrégables sur plusieurs instances (données partielles par pod)

### Symptômes

- Métriques qui "repartent de 0" à chaque déploiement
- Dashboards incorrects en environnement multi-instance

### Bonnes pratiques / mitigations

- V1 low-cost : `Redis INCRBY` best-effort par `eventType` → persisté et agrégé multi-instances
- Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ)
- Contexte technique : Redis / NestJS — 09-03-2026

---

<a id="risque-ttl-redis-heure-locale"></a>
## TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)

### Risques

- Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC

### Symptômes

- Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
- Comportement différent en dev local (TZ machine) et en prod (TZ container)

### Bonnes pratiques / mitigations

```typescript
// ✅ CORRECT — UTC midnight garanti
const midnight = new Date(
  Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
);
const ttlMs = midnight.getTime() - now.getTime();

// ❌ RISQUÉ — heure locale du serveur
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
```

- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review

- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026

---

<a id="risque-compensation-incrby-non-atomique"></a>
## Compensation `incrBy(-1)` non-atomique après dépassement de quota

### Risques

- Pattern courant de quota Redis : `INCR` → vérifier `> limit` → si oui, décrémenter (`incrBy(-1)`) et lever 429. La compensation n'est PAS atomique avec le check.
- Si Redis devient indisponible (flap) entre l'incrément et la compensation, l'appel de décrément échoue silencieusement et retourne `null`. Le compteur reste incrémenté → quota fantôme jusqu'à l'expiration de la clé.

### Symptômes

- Utilisateur bloqué par le quota alors qu'il n'a pas atteint la limite réelle
- Aucune erreur visible : la valeur de retour `null` du décrément n'est pas vérifiée
- Comportement intermittent corrélé aux instabilités Redis

### Bonnes pratiques / mitigations

```typescript
// ❌ Compensation non vérifiée — échec silencieux possible
await redis.incr(key);
if (current > limit) {
  await redis.incrBy(key, -1); // retourne null si Redis flap → compensation perdue
  throw quotaExceeded();
}

// ✅ Vérifier le retour de la compensation et loguer
const compensated = await redis.incrBy(key, -1);
if (compensated === null) {
  logger.error({ type: 'quota', event: 'compensation_failed', key });
}
```

- **Règle** : toujours vérifier le retour du décrément de compensation et loguer explicitement si `null`. Documenter ce choix dans les Dev Notes de la story (comportement intentionnel vs bug).
- **Solution robuste** : encapsuler incrément + compensation conditionnelle dans le même pipeline `MULTI/EXEC` ou un script Lua (atomicité garantie), au prix d'une complexité plus élevée.

#### Compenser AUSSI quand la transaction DB échoue (pas seulement au dépassement)

Le même compteur doit être compensé quand l'écriture DB qui suit le quota échoue. Pattern récurrent : `consumeDailyQuota` (incrément Redis) est appelé **avant** une transaction DB. Si la transaction throw, le compteur du user est consommé sans avoir produit le side-effect attendu → quota fantôme. Le piège : la compensation `incrBy(-1)` existante n'est souvent déclenchée **que** sur dépassement de quota, pas sur exception de la transaction.

```typescript
// ❌ si $transaction throw, le compteur reste incrémenté → quota fantôme
await consumeDailyQuota({ ..., action: 'dm-message' });
const message = await prisma.$transaction(async (tx) => { /* INSERT, peut échouer */ });

// ✅ compensation systématique sur exception de la transaction
await consumeDailyQuota({ ... });
try {
  const message = await prisma.$transaction(...);
} catch (err) {
  await redis.incrBy(quotaKey, -1).catch(() => {});
  throw err;
}
```

- **Trade-off** : garder l'ordre `quota → tx → compensation` (et non « tx puis quota ») garantit qu'on ne dépasse pas la limite sous charge concurrente (deux requêtes simultanées qui passeraient toutes deux le check). La compensation sur catch est donc **obligatoire**.
- **Règle** : tout flow `consumeDailyQuota` puis écriture DB doit compenser sur exception. Vérifier aussi les autres actions partageant ce flow (`comment`, `post`, `support_ticket`).

- Contexte technique : Redis / NestJS / Prisma — app-alexandrie 01-04-2026, complété 13-05-2026 (story 10.2)

---

<a id="risque-rate-limit-compteur-partage"></a>
## Rate-limit à compteur partagé entre endpoints jumeaux

### Risques

- Un helper de rate-limit dont la clé Redis omet un discriminant d'endpoint (`quota:${action}:${userId}:${jour}`) fait partager **un unique compteur** à plusieurs endpoints réutilisant le même `action` et le même discriminant (ex : l'IP).
- Les seuils respectifs des endpoints perdent tout sens : un excès sur l'un consomme le quota de l'autre. Ex : un excès de `login` bloque un `reset` de mot de passe légitime.

### Symptômes

- Un endpoint renvoie 429 alors que son propre seuil n'est pas atteint, à cause du trafic sur un endpoint jumeau.
- Le bug est invisible en test : chaque e2e exerce un seul endpoint à la fois, donc le compteur n'est jamais partagé pendant un test.

### Bonnes pratiques / mitigations

- **Règle de clé** : `clé = quota:<action>:<endpoint>:<identité>:<fenêtre>`. La clé DOIT inclure un discriminant d'endpoint, pas seulement l'identité de l'appelant.
- **Test de non-régression** : exercer DEUX endpoints jumeaux jusqu'au seuil dans le **même** test — un test mono-endpoint ne révèle jamais ce bug.

- Contexte technique : Redis / NestJS — app-alexandrie 22-05-2026
