Tous les articles
·Ingénierie·9 min

Redis et Upstash dans Next.js : rate limiting, cache partagé et sessions

Intégrez Redis via Upstash dans vos apps Next.js App Router : rate limiting Edge, cache partagé entre instances et gestion de sessions sans DB.

Déployer une application Next.js en production sur plusieurs instances crée immédiatement un problème : la mémoire de chaque instance est isolée. Un cache Map() en module-scope, un compteur de rate limiting stocké en variable, des sessions stockées uniquement en cookie — tout ça se désynchronise dès le premier redéploiement ou quand vous passez à deux pods.

Redis est la solution canonique à ce problème. Et Upstash en est la version serverless : pas de serveur à gérer, facturation à la requête, compatible Edge Runtime. C'est le binôme idéal pour une app Next.js moderne qui a besoin d'état partagé sans se transformer en infrastructure à maintenir.

Cet article couvre trois cas concrets : rate limiting dans le Middleware, cache partagé entre instances pour vos Route Handlers, et sessions légères avec révocation côté serveur.

Architecture d'une app Next.js avec Redis Upstash comme couche d'état partagé pour le rate limiting et le cache entre plusieurs instances
Redis Upstash comme couche d'état partagé entre les instances Next.js

Pourquoi Redis plutôt qu'une solution in-memory

Next.js App Router encourage les composants serveur stateless, mais certains besoins sont intrinsèquement stateful : limiter le nombre de requêtes par IP, invalider un cache sur plusieurs instances simultanément, partager des données de session entre le serveur et le Middleware Edge.

Un Map() global en Node.js ne survit pas à un redéploiement. Avec plusieurs instances ou l'Edge Runtime qui crée des isolates éphémères, il n'est tout simplement pas partagé. Redis résout ça en étant externe à vos processus.

Upstash apporte deux choses supplémentaires : une API REST (donc compatible Edge Runtime, sans connexion TCP persistante), et un SDK TypeScript @upstash/redis qui wrape cette API proprement.

// lib/redis.ts
import { Redis } from '@upstash/redis'

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

C'est tout l'initialisation. Le client est singleton-safe : l'importer dans plusieurs modules ne crée pas plusieurs connexions (contrairement à un client TCP classique comme ioredis).

Rate limiting dans le Middleware avec @upstash/ratelimit

Le Middleware Next.js est l'endroit idéal pour le rate limiting : il s'exécute avant vos Route Handlers, et avec Upstash il reste compatible Edge Runtime. Le package @upstash/ratelimit implémente plusieurs algorithmes (sliding window, fixed window, token bucket) directement sur Redis.

// 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(),
  // 20 requêtes par fenêtre glissante de 10 secondes par IP
  limiter: Ratelimit.slidingWindow(20, '10 s'),
  analytics: true,
})

export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next()
  }

  const ip = request.ip ?? '127.0.0.1'
  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(),
        'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
      },
    })
  }

  const response = NextResponse.next()
  response.headers.set('X-RateLimit-Remaining', remaining.toString())
  return response
}

export const config = {
  matcher: ['/api/:path*'],
}

slidingWindow est plus précis qu'une fenêtre fixe (qui autorise des pics au changement de fenêtre) mais consomme deux opérations Redis par requête. Pour une API publique à fort trafic, fixedWindow est suffisant et moins coûteux. L'option analytics: true stocke des métriques dans Redis — consultables depuis le dashboard Upstash.

Cache partagé entre instances pour les Route Handlers

La directive use cache (Next.js 15+) cache au niveau de l'instance. Dès que vous avez plusieurs pods ou que vous voulez invalider manuellement via un webhook, il faut un cache externe.

Pattern avec Redis comme cache L2 :

// lib/cache.ts
import { redis } from './redis'

export async function cachedFetch<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlSeconds = 60
): Promise<T> {
  const cached = await redis.get<T>(key)
  if (cached !== null) return cached

  const data = await fetcher()
  await redis.set(key, data, { ex: ttlSeconds })
  return data
}

export async function invalidateCache(...keys: string[]) {
  if (keys.length > 0) {
    await redis.del(...keys)
  }
}

Dans un Route Handler :

// app/api/products/route.ts
import { cachedFetch, invalidateCache } from '@/lib/cache'
import { db } from '@/lib/db'

export async function GET() {
  const products = await cachedFetch(
    'products:all',
    () => db.product.findMany({ where: { active: true } }),
    300 // 5 minutes
  )
  return Response.json(products)
}

// Webhook CMS → invalide le cache sur toutes les instances
export async function POST() {
  await invalidateCache('products:all')
  return Response.json({ invalidated: true })
}

Contrairement à revalidateTag de Next.js qui invalide le cache de l'instance courante uniquement, cette invalidation Redis touche toutes les instances simultanément. C'est la différence clé pour un CMS headless avec webhook.

Pour les stratégies ISR et le caching natif Next.js, voir l'article sur le caching Next.js App Router — les deux niveaux de cache sont complémentaires.

Sessions légères avec révocation côté serveur

Les sessions JWT stockées uniquement en cookie ont une limite critique : pas de révocation côté serveur. Logout = cookie supprimé, mais le token reste valide jusqu'à expiration. Redis permet une session hybride : le cookie contient un ID de session opaque, Redis stocke les données de session avec un TTL.

// lib/session.ts
import { redis } from './redis'
import { cookies } from 'next/headers'

const SESSION_TTL = 60 * 60 * 24 * 7 // 7 jours en secondes

export type SessionData = {
  userId: string
  role: 'user' | 'admin'
}

export async function getSession(): Promise<SessionData | null> {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get('session-id')?.value
  if (!sessionId) return null

  return redis.get<SessionData>(`session:${sessionId}`)
}

export async function createSession(data: SessionData): Promise<string> {
  const sessionId = crypto.randomUUID()
  await redis.set(`session:${sessionId}`, data, { ex: SESSION_TTL })
  return sessionId
}

export async function revokeSession(sessionId: string) {
  // Révocation immédiate — le cookie devient inopérant sur le prochain appel
  await redis.del(`session:${sessionId}`)
}

Le logout devient une suppression de clé Redis : même si le cookie existe encore côté client, getSession() retourne null à la prochaine requête. On peut aussi révoquer toutes les sessions d'un utilisateur en indexant les IDs dans un Set Redis.

Pour le pattern complet d'authentification Next.js et le choix entre JWT, sessions Redis et solutions tierce-partie, voir l'article sur l'authentification Next.js avec Clerk, AuthJS et Better Auth.

En pratique : coûts, limites et alternatives

Upstash est gratuit jusqu'à 10 000 commandes par jour, puis facturation à 0,2 USD pour 100 000 commandes. Pour la majorité des projets en phase de démarrage, le tier gratuit couvre largement le besoin.

Limites à connaître. L'API REST d'Upstash ajoute une latence réseau par rapport à une connexion TCP Redis directe (~5-20 ms selon la région). Pour des opérations critiques en temps réel, un Redis managé (Railway, Render, ou self-hosted) avec ioredis et le runtime Node.js classique sera plus rapide — mais incompatible Edge Runtime.

redis.keys('pattern:*') en production sur un dataset volumineux est dangereux (bloque Redis le temps du scan complet). Préférez des structures de données Redis adaptées : un Set pour indexer vos clés de cache par tag, ou la commande SCAN pour les grandes collections.

Alternative self-hosted. Si vous gérez votre propre infrastructure, Valkey — le fork open-source de Redis maintenu par la Linux Foundation — est une option viable. Upstash reste intéressant pour son Edge compatibility et son mode serverless sans connexion persistante à gérer dans un environnement stateless.

Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit ou d'un renfort tech ? Parlons-en.

Sources