
Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.
Rate limiting sur Next.js: edge middleware ou route handler, ce qu'on a fini par choisir
- secu
On a testé le rate limiting dans le middleware Edge et dans les route handlers. Les deux ont des limitations que la documentation ne mentionne pas. Voici notre arbitrage.
Rate limiting sur Next.js: edge middleware ou route handler, ce qu'on a fini par choisir
On a ajouté du rate limiting sur un projet après qu'un endpoint d'envoi d'email a été utilisé pour envoyer 12 000 messages en 20 minutes depuis 3 adresses IP. Le coût Sendgrid de ce lundi matin : 340 euros. Le temps pour nettoyer les logs et bloquer les IPs manuellement : 4 heures.
La question n'était plus "est-ce qu'on rate-limite" mais "où et comment".
Le problème avec le rate limiting dans Next.js
Next.js n'a pas de système de rate limiting intégré. C'est normal, c'est un framework, pas un reverse proxy. Mais cela signifie qu'on doit choisir où l'implémenter, et ce choix a des conséquences sur le comportement, les performances, et les limites.
Deux endroits logiques : le middleware (middleware.ts, qui tourne sur l'Edge Runtime) et les route handlers (qui tournent en Node.js).
Option 1 : middleware Edge
Le middleware s'exécute avant chaque requête, sur le réseau de distribution de Vercel (ou sur le serveur en self-hosted). C'est l'endroit idéal en théorie : on intercepte tôt, on évite d'atteindre la logique applicative.
Le problème : le middleware tourne dans l'Edge Runtime. Pas de Node.js, pas de modules natifs. Les solutions de rate limiting qui s'appuient sur Redis doivent utiliser des clients compatibles Edge.
Upstash propose précisément cela avec @upstash/ratelimit :
// middleware.ts
import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
import { NextRequest, NextResponse } from "next/server"
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, "1 m"),
analytics: true,
})
export async function middleware(request: NextRequest) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown"
const { success, limit, remaining, reset } = await ratelimit.limit(ip)
if (!success) {
return new NextResponse("Too Many Requests", {
status: 429,
headers: {
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
"Retry-After": Math.ceil((reset - Date.now()) / 1000).toString(),
},
})
}
return NextResponse.next()
}
export const config = {
matcher: ["/api/:path*"],
}Cela fonctionne. Mais deux problèmes émergent rapidement.
Le premier : le rate limiting est global sur toutes les routes /api/*. Un utilisateur légitime qui fait beaucoup de requêtes légitimes (pagination, recherche en temps réel) peut se retrouver bloqué avec les mêmes limites qu'un attaquant. On est obligés de définir des limites très permissives pour ne pas bloquer les usages normaux, ce qui réduit l'efficacité contre les attaques.
Le second : l'IP est peu fiable comme identifiant. Sur un réseau d'entreprise ou derrière un NAT, des dizaines d'utilisateurs partagent la même IP. Sur Vercel, x-forwarded-for peut être manipulé si on n'est pas derrière un proxy de confiance.
Option 2 : route handler
Dans un route handler, on a accès au Node.js runtime complet. On peut utiliser n'importe quel client Redis, accéder à la session de l'utilisateur, ou combiner plusieurs identifiants (IP + userId + action).
// app/api/send-email/route.ts
import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
import { auth } from "@/lib/auth"
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(5, "1 h"),
})
export async function POST(request: Request) {
const session = await auth()
const identifier = session?.user?.id ?? request.headers.get("x-forwarded-for") ?? "anon"
const { success, remaining } = await ratelimit.limit(
`send-email:${identifier}`
)
if (!success) {
return Response.json(
{ error: "Limite d'envoi atteinte. Réessayez dans une heure." },
{ status: 429 }
)
}
// logique d'envoi
}L'avantage est clair : on rate-limite par utilisateur authentifié, avec une limite adaptée à l'action (5 emails par heure vs 20 requêtes par minute pour l'API générale). Les utilisateurs légitimes ne sont pas pénalisés par les autres.
L'inconvénient : la requête atteint le runtime Node.js avant d'être bloquée. Sur un endpoint qui instancie une connexion base de données, c'est du compute consommé pour rien pendant une attaque.
Ce qu'on a fini par faire
On utilise les deux, à des niveaux différents.
Dans le middleware, on applique un rate limiting très permissif sur les routes API anonymes (200 requêtes par minute par IP). C'est une protection contre le flood brut, pas contre les abus subtils. Les limites sont suffisamment hautes pour ne jamais bloquer un utilisateur légitime.
Dans chaque route handler sensible, on applique un rate limiting métier : par utilisateur, par action, avec des limites adaptées au cas d'usage. L'envoi d'email a ses propres limites, la réinitialisation de mot de passe aussi, le téléchargement de fichiers aussi.
Middleware Edge
└── Filtre : > 200 req/min par IP → 429
└── Route handler
└── Auth (session user)
└── Rate limit métier : 5 emails/heure par userId
└── Logique applicativeCette architecture en deux couches évite deux écueils opposés : bloquer des utilisateurs légitimes avec des limites trop strictes dans le middleware, et laisser passer un flood sur les routes anonymes.
Alternatives à Upstash
Upstash est pratique parce qu'il propose un Redis serverless avec une API HTTP compatible Edge. Mais ce n'est pas la seule option.
Si l'infrastructure inclut déjà un Redis accessible depuis le middleware Edge (rare en self-hosted), ioredis avec un wrapper HTTP maison peut faire l'affaire. Sur des projets avec Cloudflare en frontal, Cloudflare Rate Limiting est une option plus appropriée et moins coûteuse que de gérer soi-même le state distribué.
Pour les projets où la précision de l'IP est critique (détection de fraude, protection de formulaires de paiement), combiner le rate limiting avec un service de scoring d'IP (Cloudflare Turnstile, Arcjet) donne de meilleurs résultats qu'un simple compteur par IP.
Ce qu'on n'a pas résolu
Le rate limiting par IP dans un environnement IPv6 est plus complexe. Les plages IPv6 sont larges ; un attaquant peut rotation sur des milliers d'adresses dans le même /48 sans effort. On rate-limite les /64 et non les adresses individuelles, mais c'est un paramètre qu'il faut configurer explicitement et qu'Upstash ne gère pas nativement.
La vraie question que chaque projet devrait se poser : est-ce qu'un rate limiting applicatif est suffisant, ou est-ce qu'on a besoin d'un WAF devant (Cloudflare, AWS WAF) pour les patterns d'attaque plus sophistiqués ?