Aller au contenu principal
Tous les articles
·7 min de lecture

Rate limiting Next.js avec Upstash Redis : protéger votre API en 10 min

Implémenter un rate limiting production-ready dans Next.js et tRPC, avec Upstash Redis serverless. Patterns par route, par utilisateur, par organisation.

Pourquoi vous en avez besoin (même en early-stage)

Sans rate limiting, votre SaaS est exposé à 3 attaques courantes :

  1. Brute force sur le login — un attaquant teste 10 000 mots de passe en 30 secondes
  2. Abus d'API par utilisateur — un client teste votre limite freemium en boucle
  3. Spike de coût IA — un script appelle votre route Mistral 50k fois et brûle 800€

Le rate limiting est l'une des protections au plus haut ratio impact/effort. 10 minutes pour câbler Upstash, 0 maintenance après.

Pourquoi Upstash (et pas Redis self-hosted)

Upstash Redis est serverless : tarification à la requête (1$/100k requêtes), pas de cluster à maintenir, latence < 10ms en EU.

Pour le rate limiting d'un SaaS Next.js sur Vercel, c'est le combo parfait :

  • Vercel Functions = serverless, sans état → Redis externe nécessaire
  • Upstash = serverless aussi, sans connexion persistante (HTTP/REST)
  • Aucun pool de connexion à gérer
pnpm add @upstash/redis @upstash/ratelimit
// src/lib/redis.ts
import { Redis } from "@upstash/redis";
import { env } from "~/env";
 
export const redis = new Redis({
  url: env.UPSTASH_REDIS_REST_URL,
  token: env.UPSTASH_REDIS_REST_TOKEN,
});

Pattern 1 — Rate limit par IP sur le login

Le plus critique. Brute force sur /api/auth/signin est l'attaque #1.

// src/lib/rate-limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { redis } from "./redis";
 
export const loginLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 tentatives / 15 min
  analytics: true,
  prefix: "ratelimit:login",
});
// src/app/api/auth/[...nextauth]/route.ts
import { loginLimiter } from "~/lib/rate-limit";
 
export async function POST(req: Request) {
  const ip =
    req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "anonymous";
 
  const { success, limit, remaining, reset } = await loginLimiter.limit(ip);
 
  if (!success) {
    return new Response(
      JSON.stringify({
        error: "Trop de tentatives. Réessayez dans quelques minutes.",
      }),
      {
        status: 429,
        headers: {
          "X-RateLimit-Limit": String(limit),
          "X-RateLimit-Remaining": String(remaining),
          "X-RateLimit-Reset": String(reset),
          "Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
        },
      },
    );
  }
 
  // ... reste du handler auth
}

Pattern 2 — Rate limit par utilisateur dans tRPC

Pour les routes authentifiées, on rate-limit par user ID, pas par IP (un user peut changer d'IP, et des users peuvent partager une IP en NAT corporate).

// src/lib/rate-limit.ts
export const trpcLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(100, "1 m"), // 100 req/min par user
  prefix: "ratelimit:trpc",
});
// src/server/api/trpc.ts
import { trpcLimiter } from "~/lib/rate-limit";
 
export const protectedProcedure = t.procedure.use(
  async ({ ctx, next, path }) => {
    if (!ctx.session?.user) {
      throw new TRPCError({ code: "UNAUTHORIZED" });
    }
 
    // Rate limit par user + endpoint
    const key = `${ctx.session.user.id}:${path}`;
    const { success, reset } = await trpcLimiter.limit(key);
 
    if (!success) {
      throw new TRPCError({
        code: "TOO_MANY_REQUESTS",
        message: `Réessayez dans ${Math.ceil((reset - Date.now()) / 1000)}s`,
      });
    }
 
    return next({ ctx: { ...ctx, session: ctx.session } });
  },
);

Pattern 3 — Rate limit par organisation pour l'IA

Les routes IA coûtent cher. Un utilisateur peut consommer le quota mensuel de toute son organisation en 10 minutes s'il est mal intentionné (ou s'il boucle un script).

export const aiLimiter = new Ratelimit({
  redis,
  // 30 appels IA / heure / organisation
  limiter: Ratelimit.slidingWindow(30, "1 h"),
  prefix: "ratelimit:ai",
});
 
// Dans une procédure tRPC
generateContent: requirePermission("ai:use")
  .input(z.object({ prompt: z.string() }))
  .mutation(async ({ ctx, input }) => {
    const orgId = ctx.session.user.organizationId;
    const { success } = await aiLimiter.limit(orgId);
 
    if (!success) {
      throw new TRPCError({
        code: "TOO_MANY_REQUESTS",
        message: "Limite IA horaire atteinte pour votre organisation",
      });
    }
 
    return generateWithMistral(input.prompt);
  }),

Pattern 4 — Rate limit sur les webhooks publics

Vos endpoints /api/webhooks/stripe et /api/webhooks/resend ne doivent jamais être saturés par du spam.

export const webhookLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.tokenBucket(50, "10 s", 100), // burst 100, recharge 50/10s
  prefix: "ratelimit:webhook",
});
 
export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "unknown";
  const { success } = await webhookLimiter.limit(`stripe:${ip}`);
  if (!success) return new Response("Rate limited", { status: 429 });
 
  // ... vérification signature HMAC + traitement
}

Token bucket est le bon algorithme pour les webhooks : permet des bursts légitimes (Stripe peut envoyer 50 events en 1 seconde après une migration) tout en bloquant le spam soutenu.

Pattern 5 — Whitelisting et bypass

Pour les tests E2E ou les comptes admin, on veut bypass le rate limit.

const WHITELIST_IPS = new Set(env.RATE_LIMIT_WHITELIST?.split(",") ?? []);
 
export async function checkRateLimit(
  limiter: Ratelimit,
  identifier: string,
  ip: string,
) {
  if (WHITELIST_IPS.has(ip)) return { success: true };
 
  return limiter.limit(identifier);
}

Variables d'environnement :

# .env.local — pour le dev
RATE_LIMIT_WHITELIST="127.0.0.1,::1"
 
# Vercel Production
RATE_LIMIT_WHITELIST="" # vide en prod

Monitoring

Upstash Console offre des graphes built-in (analytics: true). Pour aller plus loin, exposez vos métriques en Prometheus ou Vercel Analytics :

import { track } from "@vercel/analytics/server";
 
if (!success) {
  await track("rate_limit_hit", {
    limiter: "trpc",
    userId: ctx.session.user.id,
  });
  throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
}

Configuration recommandée pour un SaaS B2B

EndpointLimiteAlgorithmeClé
/api/auth/signin5 / 15minsliding windowIP
/api/auth/register3 / 1hsliding windowIP
/api/auth/forgot-password3 / 1hsliding windowemail
tRPC protected100 / 1minsliding windowuserId+path
tRPC AI30 / 1hsliding windoworgId
Webhooks publics100 burst, 50/10stoken bucketIP
API publique (clé API)1000 / 1minsliding windowapiKey

Coût Upstash typique

Pour un SaaS de 1000 utilisateurs actifs :

  • ~500 req/sec en pic
  • ~50M req/mois sur les rate limiters

Upstash facture 0.2$/100k commands → **~100$/mois**. Si ça vous semble cher, vous pouvez choisir le plan "Pay-as-you-go" jusqu'à 10k req/jour gratuites, puis 0.2$/100k.

Pour la plupart des SaaS, 5-20$/mois suffit largement.

Erreurs à éviter

  1. Bypasser le rate limit en dev par paresse — testez vos limites en local, c'est là qu'on découvre les UI qui spamment l'API en boucle.
  2. Une seule clé globale — un attaquant qui sature 1 endpoint sature tout. Utilisez des prefix différents par endpoint.
  3. Pas de header Retry-After — votre frontend ne sait pas quand retry. Toujours retourner reset dans la réponse.
  4. Rate limit le seul fait des Server Actions — les Server Actions Next.js sont exposées sur des URLs prédictibles. Rate-limitez les Actions sensibles (paiement, envoi d'email) comme les API routes.
  5. Stocker la clé Redis dans le code — utilisez env.js et T3 Env pour valider que les variables sont là au boot.

Conclusion

10 minutes pour câbler Upstash, 30 minutes pour configurer les 5-7 limiters dont votre SaaS a besoin, et vous êtes protégé contre 90% des abus courants. Le coût est négligeable, l'UX impact est nul (les vrais utilisateurs ne se font jamais limiter avec ces seuils).

HeartCo Starter inclut Upstash pré-câblé avec ces 5 patterns dans src/lib/rate-limit.ts. Vous décommentez les limiters dont vous avez besoin et vous oubliez le sujet.

Partager
Rate limiting Next.js avec Upstash Redis : protéger votre API en 10 min | HeartCo Dev Blog